diff --git a/bin/drk/dao.sql b/bin/drk/dao.sql index 524f380b0..867392ba4 100644 --- a/bin/drk/dao.sql +++ b/bin/drk/dao.sql @@ -25,7 +25,7 @@ -- $ drk dao import_dao DAO_NAME < dao.dat -- Imported DAO ccb8XXX8af6 -- --- Where ccb8XXX8af6 is the DAO's bulla. +-- Where ccb8XXX8af6 is the DAO's name. -- -- Next one person will mint it on chain -- @@ -104,22 +104,16 @@ PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_daos ( - dao_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - name BLOB UNIQUE NOT NULL, - proposer_limit BLOB NOT NULL, - -- minimum threshold for total number of votes for proposal to pass. - -- If there's too little activity then it cannot pass. - quorum BLOB NOT NULL, - -- Needed ratio of yes/total for proposal to pass. - -- approval_ratio = approval_ratio_quot / approval_ratio_base - approval_ratio_base INTEGER NOT NULL, - approval_ratio_quot INTEGER NOT NULL, - gov_token_id BLOB NOT NULL, - secret BLOB NOT NULL, - bulla_blind BLOB NOT NULL, - -- these values are NULL until the DAO is minted on chain and received - leaf_position BLOB, + -- Name identifier of the DAO + name TEXT PRIMARY KEY UNIQUE NOT NULL, + -- DAO parameters + params BLOB NOT NULL, + -- These values are NULL until the DAO is minted on chain and received + -- Leaf position of the DAO in the Merkle tree of DAOs + leaf_position BLOB, + -- The transaction hash where the DAO was deployed tx_hash BLOB, + -- The call index in the transaction where the DAO was deployed call_index INTEGER ); @@ -131,7 +125,7 @@ CREATE TABLE IF NOT EXISTS Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_tree CREATE TABLE IF NOT EXISTS Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_proposals ( proposal_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - dao_id INTEGER NOT NULL, + dao_name TEXT NOT NULL, -- Public key of person that would receive the funds recv_public BLOB NOT NULL, -- Amount of funds that would be sent @@ -141,15 +135,15 @@ CREATE TABLE IF NOT EXISTS Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_prop bulla_blind BLOB NOT NULL, -- these values are NULL until the proposal is minted on chain -- and received by the DAO - leaf_position BLOB, - money_snapshot_tree BLOB, + leaf_position BLOB, + money_snapshot_tree BLOB, tx_hash BLOB, call_index INTEGER, -- this is NULL until we have voted on this proposal our_vote_id INTEGER UNIQUE, FOREIGN KEY(our_vote_id) REFERENCES Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_votes(vote_id) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY(dao_id) REFERENCES Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_daos(dao_id) ON DELETE CASCADE ON UPDATE CASCADE + FOREIGN KEY(dao_name) REFERENCES Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_daos(name) ON DELETE CASCADE ON UPDATE CASCADE ); CREATE TABLE IF NOT EXISTS Fd8kfCuqU8BoFFp6GcXv5pC8XXRkBK7gUPQX5XDz7iXj_dao_votes ( diff --git a/bin/drk/src/cli_util.rs b/bin/drk/src/cli_util.rs index 12f662da0..b617d964b 100644 --- a/bin/drk/src/cli_util.rs +++ b/bin/drk/src/cli_util.rs @@ -264,24 +264,23 @@ pub fn generate_completions(shell: &str) -> Result<()> { let name = Arg::with_name("name").help("Name identifier for the DAO"); - let import = - SubCommand::with_name("import").about("Import DAO data from stdin").args(&vec![name]); + let import = SubCommand::with_name("import") + .about("Import DAO data from stdin") + .args(&vec![name.clone()]); - let name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)"); + let opt_name = Arg::with_name("dao-alias").help("Name identifier for the DAO (optional)"); let list = SubCommand::with_name("list") .about("List imported DAOs (or info about a specific one)") - .args(&vec![name]); - - let dao_alias = Arg::with_name("dao-alias").help("Name or numeric identifier for the DAO"); + .args(&vec![opt_name]); let balance = SubCommand::with_name("balance") .about("Show the balance of a DAO") - .args(&vec![dao_alias.clone()]); + .args(&vec![name.clone()]); let mint = SubCommand::with_name("mint") .about("Mint an imported DAO on-chain") - .args(&vec![dao_alias.clone()]); + .args(&vec![name.clone()]); let recipient = Arg::with_name("recipient").help("Pubkey to send tokens to with proposal success"); @@ -292,17 +291,16 @@ pub fn generate_completions(shell: &str) -> Result<()> { let propose = SubCommand::with_name("propose") .about("Create a proposal for a DAO") - .args(&vec![dao_alias.clone(), recipient, amount, token]); + .args(&vec![name.clone(), recipient, amount, token]); - let proposals = SubCommand::with_name("proposals") - .about("List DAO proposals") - .args(&vec![dao_alias.clone()]); + let proposals = + SubCommand::with_name("proposals").about("List DAO proposals").args(&vec![name.clone()]); let proposal_id = Arg::with_name("proposal-id").help("Numeric identifier for the proposal"); let proposal = SubCommand::with_name("proposal") .about("View a DAO proposal data") - .args(&vec![dao_alias.clone(), proposal_id.clone()]); + .args(&vec![name.clone(), proposal_id.clone()]); let vote = Arg::with_name("vote").help("Vote (0 for NO, 1 for YES)"); @@ -310,7 +308,7 @@ pub fn generate_completions(shell: &str) -> Result<()> { Arg::with_name("vote-weight").help("Vote weight (amount of governance tokens)"); let vote = SubCommand::with_name("vote").about("Vote on a given proposal").args(&vec![ - dao_alias.clone(), + name.clone(), proposal_id.clone(), vote, vote_weight, @@ -318,7 +316,7 @@ pub fn generate_completions(shell: &str) -> Result<()> { let exec = SubCommand::with_name("exec") .about("Execute a DAO proposal") - .args(&vec![dao_alias, proposal_id]); + .args(&vec![name, proposal_id]); let dao = SubCommand::with_name("dao").about("DAO functionalities").subcommands(vec![ create, view, import, list, balance, mint, propose, proposals, proposal, vote, exec, diff --git a/bin/drk/src/dao.rs b/bin/drk/src/dao.rs index d476c012a..3c995131f 100644 --- a/bin/drk/src/dao.rs +++ b/bin/drk/src/dao.rs @@ -31,12 +31,14 @@ use darkfi::{ }; use darkfi_dao_contract::{ client::{make_mint_call, DaoProposeCall, DaoProposeStakeInput, DaoVoteCall, DaoVoteInput}, - model::{DaoAuthCall, DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams}, + model::{Dao, DaoAuthCall, DaoBulla, DaoMintParams, DaoProposeParams, DaoVoteParams}, DaoFunction, DAO_CONTRACT_ZKAS_DAO_MINT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_PROPOSE_MAIN_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_INPUT_NS, DAO_CONTRACT_ZKAS_DAO_VOTE_MAIN_NS, }; -use darkfi_money_contract::{client::OwnCoin, model::TokenId, MoneyFunction}; +use darkfi_money_contract::{ + client::OwnCoin, model::TokenId, MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1, +}; use darkfi_sdk::{ bridgetree, crypto::{ @@ -74,15 +76,8 @@ lazy_static! { } // DAO_DAOS_TABLE -pub const DAO_DAOS_COL_DAO_ID: &str = "dao_id"; pub const DAO_DAOS_COL_NAME: &str = "name"; -pub const DAO_DAOS_COL_PROPOSER_LIMIT: &str = "proposer_limit"; -pub const DAO_DAOS_COL_QUORUM: &str = "quorum"; -pub const DAO_DAOS_COL_APPROVAL_RATIO_BASE: &str = "approval_ratio_base"; -pub const DAO_DAOS_COL_APPROVAL_RATIO_QUOT: &str = "approval_ratio_quot"; -pub const DAO_DAOS_COL_GOV_TOKEN_ID: &str = "gov_token_id"; -pub const DAO_DAOS_COL_SECRET: &str = "secret"; -pub const DAO_DAOS_COL_BULLA_BLIND: &str = "bulla_blind"; +pub const DAO_DAOS_COL_PARAMS: &str = "params"; pub const DAO_DAOS_COL_LEAF_POSITION: &str = "leaf_position"; pub const DAO_DAOS_COL_TX_HASH: &str = "tx_hash"; pub const DAO_DAOS_COL_CALL_INDEX: &str = "call_index"; @@ -97,7 +92,7 @@ pub const _DAO_COINS_COL_DAO_ID: &str = "dao_id"; // DAO_PROPOSALS_TABLE pub const DAO_PROPOSALS_COL_PROPOSAL_ID: &str = "proposal_id"; -pub const DAO_PROPOSALS_COL_DAO_ID: &str = "dao_id"; +pub const DAO_PROPOSALS_COL_DAO_NAME: &str = "dao_name"; pub const DAO_PROPOSALS_COL_RECV_PUBLIC: &str = "recv_public"; pub const DAO_PROPOSALS_COL_AMOUNT: &str = "amount"; pub const DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID: &str = "sendcoin_token_id"; @@ -134,19 +129,33 @@ pub struct DaoProposeNote { #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] /// Parameters representing a DAO to be initialized pub struct DaoParams { - /// The minimum amount of governance tokens needed to open a proposal - pub proposer_limit: u64, - /// Minimal threshold of participating total tokens needed for a proposal to pass - pub quorum: u64, - /// The ratio of winning/total votes needed for a proposal to pass - pub approval_ratio_base: u64, - pub approval_ratio_quot: u64, - /// DAO's governance token ID - pub gov_token_id: TokenId, + /// The on chain representation of the DAO + pub dao: Dao, /// Secret key for the DAO pub secret_key: SecretKey, - /// DAO bulla blind - pub bulla_blind: pallas::Base, +} + +impl DaoParams { + pub fn new( + proposer_limit: u64, + quorum: u64, + approval_ratio_base: u64, + approval_ratio_quot: u64, + gov_token_id: TokenId, + secret_key: SecretKey, + bulla_blind: BaseBlind, + ) -> Self { + let dao = Dao { + proposer_limit, + quorum, + approval_ratio_base, + approval_ratio_quot, + gov_token_id, + public_key: PublicKey::from_secret(secret_key), + bulla_blind, + }; + Self { dao, secret_key } + } } impl fmt::Display for DaoParams { @@ -156,21 +165,21 @@ impl fmt::Display for DaoParams { "DAO Parameters", "==============", "Proposer limit", - encode_base10(self.proposer_limit, BALANCE_BASE10_DECIMALS), - self.proposer_limit, + encode_base10(self.dao.proposer_limit, BALANCE_BASE10_DECIMALS), + self.dao.proposer_limit, "Quorum", - encode_base10(self.quorum, BALANCE_BASE10_DECIMALS), - self.quorum, + encode_base10(self.dao.quorum, BALANCE_BASE10_DECIMALS), + self.dao.quorum, "Approval ratio", - self.approval_ratio_quot as f64 / self.approval_ratio_base as f64, + self.dao.approval_ratio_quot as f64 / self.dao.approval_ratio_base as f64, "Governance Token ID", - self.gov_token_id, + self.dao.gov_token_id, "Public key", - PublicKey::from_secret(self.secret_key), + self.dao.public_key, "Secret key", self.secret_key, "Bulla blind", - self.bulla_blind, + self.dao.bulla_blind, ); write!(f, "{}", s) @@ -178,25 +187,12 @@ impl fmt::Display for DaoParams { } #[derive(Debug, Clone)] -/// Parameters representing an intialized DAO, optionally deployed on-chain -pub struct Dao { - /// Numeric identifier for the DAO - pub id: u64, - /// Named identifier for the DAO +/// Structure representing a `DAO_DAOS_TABLE` record. +pub struct DaoRecord { + /// Name identifier for the DAO pub name: String, - /// The minimum amount of governance tokens needed to open a proposal - pub proposer_limit: u64, - /// Minimal threshold of participating total tokens needed for a proposal to pass - pub quorum: u64, - /// The ratio of winning/total votes needed for a proposal to pass - pub approval_ratio_base: u64, - pub approval_ratio_quot: u64, - /// DAO's governance token ID - pub gov_token_id: TokenId, - /// Secret key for the DAO - pub secret_key: SecretKey, - /// DAO bulla blind - pub bulla_blind: BaseBlind, + /// DAO parameters + pub params: DaoParams, /// Leaf position of the DAO in the Merkle tree of DAOs pub leaf_position: Option, /// The transaction hash where the DAO was deployed @@ -205,32 +201,43 @@ pub struct Dao { pub call_index: Option, } -impl Dao { - pub fn bulla(&self) -> DaoBulla { - let (x, y) = PublicKey::from_secret(self.secret_key).xy(); +impl DaoRecord { + pub fn new( + name: String, + params: DaoParams, + leaf_position: Option, + tx_hash: Option, + call_index: Option, + ) -> Self { + Self { name, params, leaf_position, tx_hash, call_index } + } - DaoBulla::from(poseidon_hash([ - pallas::Base::from(self.proposer_limit), - pallas::Base::from(self.quorum), - pallas::Base::from(self.approval_ratio_quot), - pallas::Base::from(self.approval_ratio_base), - self.gov_token_id.inner(), - x, - y, - self.bulla_blind.inner(), - ])) + pub fn bulla(&self) -> DaoBulla { + self.params.dao.to_bulla() } pub fn keypair(&self) -> Keypair { - let public = PublicKey::from_secret(self.secret_key); - Keypair { public, secret: self.secret_key } + let public = PublicKey::from_secret(self.params.secret_key); + Keypair { public, secret: self.params.secret_key } } } -impl fmt::Display for Dao { +impl fmt::Display for DaoRecord { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let leaf_position = match self.leaf_position { + Some(p) => format!("{p:?}"), + None => "None".to_string(), + }; + let tx_hash = match self.tx_hash { + Some(t) => format!("{t}"), + None => "None".to_string(), + }; + let call_index = match self.call_index { + Some(c) => format!("{c}"), + None => "None".to_string(), + }; let s = format!( - "{}\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{}: {}\n{}: {}", "DAO Parameters", "==============", "Name", @@ -238,27 +245,27 @@ impl fmt::Display for Dao { "Bulla", self.bulla(), "Proposer limit", - encode_base10(self.proposer_limit, BALANCE_BASE10_DECIMALS), - self.proposer_limit, + encode_base10(self.params.dao.proposer_limit, BALANCE_BASE10_DECIMALS), + self.params.dao.proposer_limit, "Quorum", - encode_base10(self.quorum, BALANCE_BASE10_DECIMALS), - self.quorum, + encode_base10(self.params.dao.quorum, BALANCE_BASE10_DECIMALS), + self.params.dao.quorum, "Approval ratio", - self.approval_ratio_quot as f64 / self.approval_ratio_base as f64, + self.params.dao.approval_ratio_quot as f64 / self.params.dao.approval_ratio_base as f64, "Governance Token ID", - self.gov_token_id, + self.params.dao.gov_token_id, "Public key", - PublicKey::from_secret(self.secret_key), + self.params.dao.public_key, "Secret key", - self.secret_key, + self.params.secret_key, "Bulla blind", - self.bulla_blind, + self.params.dao.bulla_blind, "Leaf position", - self.leaf_position, + leaf_position, "Tx hash", - self.tx_hash, + tx_hash, "Call idx", - self.call_index, + call_index, ); write!(f, "{}", s) @@ -442,66 +449,25 @@ impl Drk { let daos = self.get_daos().await?; let mut ret = Vec::with_capacity(daos.len()); for dao in daos { - ret.push(dao.secret_key); + ret.push(dao.params.secret_key); } Ok(ret) } /// Auxiliary function to parse a `DAO_DAOS_TABLE` record. - async fn parse_dao_record(&self, row: &[Value]) -> Result { - let Value::Integer(id) = row[0] else { - return Err(Error::ParseFailed("[parse_dao_record] ID parsing failed")) - }; - let Ok(id) = u64::try_from(id) else { - return Err(Error::ParseFailed("[parse_dao_record] ID parsing failed")) - }; - - let Value::Text(ref name) = row[1] else { + async fn parse_dao_record(&self, row: &[Value]) -> Result { + let Value::Text(ref name) = row[0] else { return Err(Error::ParseFailed("[parse_dao_record] Name parsing failed")) }; let name = name.clone(); - let Value::Blob(ref proposer_limit_bytes) = row[2] else { - return Err(Error::ParseFailed("[parse_dao_record] Proposer limit bytes parsing failed")) + let Value::Blob(ref params_bytes) = row[1] else { + return Err(Error::ParseFailed("[parse_dao_record] Params bytes parsing failed")) }; - let proposer_limit = deserialize_async(proposer_limit_bytes).await?; + let params = deserialize_async(params_bytes).await?; - let Value::Blob(ref quorum_bytes) = row[3] else { - return Err(Error::ParseFailed("[parse_dao_record] Quorum bytes parsing failed")) - }; - let quorum = deserialize_async(quorum_bytes).await?; - - let Value::Integer(approval_ratio_base) = row[4] else { - return Err(Error::ParseFailed("[parse_dao_record] Approval ratio base parsing failed")) - }; - let Ok(approval_ratio_base) = u64::try_from(approval_ratio_base) else { - return Err(Error::ParseFailed("[parse_dao_record] Approval ratio base parsing failed")) - }; - - let Value::Integer(approval_ratio_quot) = row[5] else { - return Err(Error::ParseFailed("[parse_dao_record] Approval ratio quot parsing failed")) - }; - let Ok(approval_ratio_quot) = u64::try_from(approval_ratio_quot) else { - return Err(Error::ParseFailed("[parse_dao_record] Approval ratio quot parsing failed")) - }; - - let Value::Blob(ref gov_token_bytes) = row[6] else { - return Err(Error::ParseFailed("[parse_dao_record] Gov token bytes parsing failed")) - }; - let gov_token_id = deserialize_async(gov_token_bytes).await?; - - let Value::Blob(ref secret_bytes) = row[7] else { - return Err(Error::ParseFailed("[parse_dao_record] Secret key bytes parsing failed")) - }; - let secret_key = deserialize_async(secret_bytes).await?; - - let Value::Blob(ref bulla_blind_bytes) = row[8] else { - return Err(Error::ParseFailed("[parse_dao_record] Bulla blind bytes parsing failed")) - }; - let bulla_blind = deserialize_async(bulla_blind_bytes).await?; - - let leaf_position = match row[9] { + let leaf_position = match row[2] { Value::Blob(ref leaf_position_bytes) => { Some(deserialize_async(leaf_position_bytes).await?) } @@ -513,7 +479,7 @@ impl Drk { } }; - let tx_hash = match row[10] { + let tx_hash = match row[3] { Value::Blob(ref tx_hash_bytes) => Some(deserialize_async(tx_hash_bytes).await?), Value::Null => None, _ => { @@ -523,7 +489,7 @@ impl Drk { } }; - let call_index = match row[11] { + let call_index = match row[4] { Value::Integer(call_index) => { let Ok(call_index) = u8::try_from(call_index) else { return Err(Error::ParseFailed("[parse_dao_record] Call index parsing failed")) @@ -534,26 +500,13 @@ impl Drk { _ => return Err(Error::ParseFailed("[parse_dao_record] Call index parsing failed")), }; - let dao = Dao { - id, - name, - proposer_limit, - quorum, - approval_ratio_base, - approval_ratio_quot, - gov_token_id, - secret_key, - bulla_blind, - leaf_position, - tx_hash, - call_index, - }; + let dao = DaoRecord::new(name, params, leaf_position, tx_hash, call_index); Ok(dao) } /// Fetch all known DAOs from the wallet. - pub async fn get_daos(&self) -> Result> { + pub async fn get_daos(&self) -> Result> { let rows = match self.wallet.query_multiple(&DAO_DAOS_TABLE, &[], &[]).await { Ok(r) => r, Err(e) => { @@ -566,14 +519,11 @@ impl Drk { daos.push(self.parse_dao_record(&row).await?); } - // Here we sort the vec by ID. The SQL SELECT statement does not guarantee - // this, so just do it here. - daos.sort_by(|a, b| a.id.cmp(&b.id)); Ok(daos) } /// Auxiliary function to parse a proposal record row. - async fn parse_dao_proposal(&self, dao: &Dao, row: &[Value]) -> Result { + async fn parse_dao_proposal(&self, dao: &DaoRecord, row: &[Value]) -> Result { let Value::Integer(id) = row[0] else { return Err(Error::ParseFailed("[get_dao_proposals] ID parsing failed")) }; @@ -581,13 +531,10 @@ impl Drk { return Err(Error::ParseFailed("[get_dao_proposals] ID parsing failed")) }; - let Value::Integer(dao_id) = row[1] else { - return Err(Error::ParseFailed("[get_dao_proposals] DAO ID parsing failed")) + let Value::Text(ref dao_name) = row[1] else { + return Err(Error::ParseFailed("[get_dao_proposals] DAO name parsing failed")) }; - let Ok(dao_id) = u64::try_from(dao_id) else { - return Err(Error::ParseFailed("[get_dao_proposals] DAO ID parsing failed")) - }; - assert!(dao_id == dao.id); + assert!(dao_name == &dao.name); let dao_bulla = dao.bulla(); let Value::Blob(ref recipient_bytes) = row[2] else { @@ -675,12 +622,11 @@ impl Drk { }) } - /// Fetch all known DAO proposals from the wallet given a DAO ID. - pub async fn get_dao_proposals(&self, dao_id: u64) -> Result> { - let daos = self.get_daos().await?; - let Some(dao) = daos.get(dao_id as usize - 1) else { + /// Fetch all known DAO proposals from the wallet given a DAO name. + pub async fn get_dao_proposals(&self, name: &str) -> Result> { + let Ok(dao) = self.get_dao_by_name(name).await else { return Err(Error::RusqliteError(format!( - "[get_dao_proposals] DAO with ID {dao_id} not found in wallet" + "[get_dao_proposals] DAO with name {name} not found in wallet" ))) }; @@ -689,7 +635,7 @@ impl Drk { .query_multiple( &DAO_PROPOSALS_TABLE, &[], - convert_named_params! {(DAO_PROPOSALS_COL_DAO_ID, dao_id)}, + convert_named_params! {(DAO_PROPOSALS_COL_DAO_NAME, name)}, ) .await { @@ -703,7 +649,7 @@ impl Drk { let mut proposals = Vec::with_capacity(rows.len()); for row in rows { - let proposal = self.parse_dao_proposal(dao, &row).await?; + let proposal = self.parse_dao_proposal(&dao, &row).await?; proposals.push(proposal); } @@ -722,10 +668,6 @@ impl Drk { call_idx: u8, confirm: bool, ) -> Result<()> { - let mut daos = self.get_daos().await?; - let mut daos_to_confirm = vec![]; - let (mut daos_tree, mut proposals_tree) = self.get_dao_trees().await?; - // DAOs that have been minted let mut new_dao_bullas: Vec<(DaoBulla, Option, u8)> = vec![]; // DAO proposals that have been minted @@ -735,10 +677,8 @@ impl Drk { Option, u8, )> = vec![]; - let mut our_proposals: Vec = vec![]; // DAO votes that have been seen let mut new_dao_votes: Vec<(DaoVoteParams, Option, u8)> = vec![]; - let mut dao_votes: Vec = vec![]; // Run through the transaction and see what we got: match DaoFunction::try_from(data[0])? { @@ -775,132 +715,144 @@ impl Drk { // This code should only be executed when finalized blocks are being scanned. // Here we write the tx metadata, and actually do Merkle tree appends so we // have to make sure it's the same for everyone. - if confirm { - for new_bulla in new_dao_bullas { - daos_tree.append(MerkleNode::from(new_bulla.0.inner())); - for dao in daos.iter_mut() { - if dao.bulla() == new_bulla.0 { - println!("[apply_tx_dao_data] Found minted DAO {}, noting down for wallet update", new_bulla.0); - // We have this DAO imported in our wallet. Add the metadata: - dao.leaf_position = daos_tree.mark(); - dao.tx_hash = new_bulla.1; - dao.call_index = Some(new_bulla.2); - daos_to_confirm.push(dao.clone()); - } + if !confirm { + return Ok(()); + } + + let daos = self.get_daos().await?; + let mut daos_to_confirm = vec![]; + let (mut daos_tree, mut proposals_tree) = self.get_dao_trees().await?; + for new_bulla in new_dao_bullas { + daos_tree.append(MerkleNode::from(new_bulla.0.inner())); + for dao in &daos { + if dao.bulla() == new_bulla.0 { + println!( + "[apply_tx_dao_data] Found minted DAO {}, noting down for wallet update", + new_bulla.0 + ); + // We have this DAO imported in our wallet. Add the metadata: + let mut dao_to_confirm = dao.clone(); + dao_to_confirm.leaf_position = daos_tree.mark(); + dao_to_confirm.tx_hash = new_bulla.1; + dao_to_confirm.call_index = Some(new_bulla.2); + daos_to_confirm.push(dao_to_confirm); } } + } - for proposal in new_dao_proposals { - proposals_tree.append(MerkleNode::from(proposal.0.proposal_bulla.inner())); + let mut our_proposals: Vec = vec![]; + for proposal in new_dao_proposals { + proposals_tree.append(MerkleNode::from(proposal.0.proposal_bulla.inner())); - // If we're able to decrypt this note, that's the way to link it - // to a specific DAO. - for dao in &daos { - if let Ok(note) = proposal.0.note.decrypt::(&dao.secret_key) { - // We managed to decrypt it. Let's place this in a proper - // DaoProposal object. We assume we can just increment the - // ID by looking at how many proposals we already have. - // We also assume we don't mantain duplicate DAOs in the - // wallet. - println!("[apply_tx_dao_data] Managed to decrypt DAO proposal note"); - let daos_proposals = self.get_dao_proposals(dao.id).await?; - let our_prop = DaoProposal { - // This ID stuff is flaky. - id: daos_proposals.len() as u64 + our_proposals.len() as u64 + 1, - dao_bulla: dao.bulla(), - recipient: note.proposal.dest, - amount: note.proposal.amount, - token_id: note.proposal.token_id, - bulla_blind: note.proposal.blind, - leaf_position: proposals_tree.mark(), - money_snapshot_tree: proposal.1, - tx_hash: proposal.2, - call_index: Some(proposal.3), - vote_id: None, - }; - - our_proposals.push(our_prop); - break - } - } - } - - for vote in new_dao_votes { - for dao in &daos { - // TODO: we shouldn't decrypt with all DAOs here - let note = vote.0.note.decrypt_unsafe(&dao.secret_key)?; - println!("[apply_tx_dao_data] Managed to decrypt DAO proposal vote note"); - let daos_proposals = self.get_dao_proposals(dao.id).await?; - let mut proposal_id = None; - - for i in daos_proposals { - if i.bulla() == vote.0.proposal_bulla.inner() { - proposal_id = Some(i.id); - break - } - } - - if proposal_id.is_none() { - println!("[apply_tx_dao_data] Warning: Decrypted DaoVoteNote but did not find proposal"); - break - } - - let vote_option = fp_to_u64(note[0]).unwrap(); - assert!(vote_option == 0 || vote_option == 1); - let vote_option = vote_option != 0; - let yes_vote_blind = Blind(fp_mod_fv(note[1])); - let all_vote_value = fp_to_u64(note[2]).unwrap(); - let all_vote_blind = Blind(fp_mod_fv(note[3])); - - let v = DaoVote { - id: 0, - proposal_id: proposal_id.unwrap(), - vote_option, - yes_vote_blind, - all_vote_value, - all_vote_blind, - tx_hash: vote.1, - call_index: Some(vote.2), + // If we're able to decrypt this note, that's the way to link it + // to a specific DAO. + for dao in &daos { + if let Ok(note) = proposal.0.note.decrypt::(&dao.params.secret_key) + { + // We managed to decrypt it. Let's place this in a proper + // DaoProposal object. We assume we can just increment the + // ID by looking at how many proposals we already have. + // We also assume we don't mantain duplicate DAOs in the + // wallet. + println!("[apply_tx_dao_data] Managed to decrypt DAO proposal note"); + let daos_proposals = self.get_dao_proposals(&dao.name).await?; + let our_prop = DaoProposal { + // This ID stuff is flaky. + id: daos_proposals.len() as u64 + our_proposals.len() as u64 + 1, + dao_bulla: dao.bulla(), + recipient: note.proposal.dest, + amount: note.proposal.amount, + token_id: note.proposal.token_id, + bulla_blind: note.proposal.blind, + leaf_position: proposals_tree.mark(), + money_snapshot_tree: proposal.1, + tx_hash: proposal.2, + call_index: Some(proposal.3), + vote_id: None, }; - dao_votes.push(v); + our_proposals.push(our_prop); + break } } + } - if let Err(e) = self.put_dao_trees(&daos_tree, &proposals_tree).await { - return Err(Error::RusqliteError(format!( - "[apply_tx_dao_data] Put DAO tree failed: {e:?}" - ))) - } - if let Err(e) = self.confirm_daos(&daos_to_confirm).await { - return Err(Error::RusqliteError(format!( - "[apply_tx_dao_data] Confirm DAOs failed: {e:?}" - ))) - } - self.put_dao_proposals(&our_proposals).await?; - if let Err(e) = self.put_dao_votes(&dao_votes).await { - return Err(Error::RusqliteError(format!( - "[apply_tx_dao_data] Put DAO votes failed: {e:?}" - ))) + let mut dao_votes: Vec = vec![]; + for vote in new_dao_votes { + for dao in &daos { + // TODO: we shouldn't decrypt with all DAOs here + let note = vote.0.note.decrypt_unsafe(&dao.params.secret_key)?; + println!("[apply_tx_dao_data] Managed to decrypt DAO proposal vote note"); + let daos_proposals = self.get_dao_proposals(&dao.name).await?; + let mut proposal_id = None; + + for i in daos_proposals { + if i.bulla() == vote.0.proposal_bulla.inner() { + proposal_id = Some(i.id); + break + } + } + + if proposal_id.is_none() { + println!("[apply_tx_dao_data] Warning: Decrypted DaoVoteNote but did not find proposal"); + break + } + + let vote_option = fp_to_u64(note[0]).unwrap(); + assert!(vote_option == 0 || vote_option == 1); + let vote_option = vote_option != 0; + let yes_vote_blind = Blind(fp_mod_fv(note[1])); + let all_vote_value = fp_to_u64(note[2]).unwrap(); + let all_vote_blind = Blind(fp_mod_fv(note[3])); + + let v = DaoVote { + id: 0, + proposal_id: proposal_id.unwrap(), + vote_option, + yes_vote_blind, + all_vote_value, + all_vote_blind, + tx_hash: vote.1, + call_index: Some(vote.2), + }; + + dao_votes.push(v); } } + if let Err(e) = self.put_dao_trees(&daos_tree, &proposals_tree).await { + return Err(Error::RusqliteError(format!( + "[apply_tx_dao_data] Put DAO tree failed: {e:?}" + ))) + } + if let Err(e) = self.confirm_daos(&daos_to_confirm).await { + return Err(Error::RusqliteError(format!( + "[apply_tx_dao_data] Confirm DAOs failed: {e:?}" + ))) + } + self.put_dao_proposals(&our_proposals).await?; + if let Err(e) = self.put_dao_votes(&dao_votes).await { + return Err(Error::RusqliteError(format!( + "[apply_tx_dao_data] Put DAO votes failed: {e:?}" + ))) + } + Ok(()) } /// Confirm already imported DAO metadata into the wallet. /// Here we just write the leaf position, tx hash, and call index. /// Panics if the fields are None. - pub async fn confirm_daos(&self, daos: &[Dao]) -> WalletDbResult<()> { + pub async fn confirm_daos(&self, daos: &[DaoRecord]) -> WalletDbResult<()> { for dao in daos { let query = format!( - "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = {};", + "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = \'{}\';", *DAO_DAOS_TABLE, DAO_DAOS_COL_LEAF_POSITION, DAO_DAOS_COL_TX_HASH, DAO_DAOS_COL_CALL_INDEX, - DAO_DAOS_COL_DAO_ID, - dao.id, + DAO_DAOS_COL_NAME, + dao.name, ); self.wallet .exec_sql( @@ -918,16 +870,16 @@ impl Drk { } /// Unconfirm imported DAOs by removing the leaf position, txid, and call index. - pub async fn unconfirm_daos(&self, daos: &[Dao]) -> WalletDbResult<()> { + pub async fn unconfirm_daos(&self, daos: &[DaoRecord]) -> WalletDbResult<()> { for dao in daos { let query = format!( - "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = {};", + "UPDATE {} SET {} = ?1, {} = ?2, {} = ?3 WHERE {} = \'{}\';", *DAO_DAOS_TABLE, DAO_DAOS_COL_LEAF_POSITION, DAO_DAOS_COL_TX_HASH, DAO_DAOS_COL_CALL_INDEX, - DAO_DAOS_COL_DAO_ID, - dao.id, + DAO_DAOS_COL_NAME, + dao.name, ); self.wallet .exec_sql(&query, rusqlite::params![None::>, None::>, None::,]) @@ -951,7 +903,7 @@ impl Drk { let query = format!( "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9);", *DAO_PROPOSALS_TABLE, - DAO_PROPOSALS_COL_DAO_ID, + DAO_PROPOSALS_COL_DAO_NAME, DAO_PROPOSALS_COL_RECV_PUBLIC, DAO_PROPOSALS_COL_AMOUNT, DAO_PROPOSALS_COL_SENDCOIN_TOKEN_ID, @@ -967,7 +919,7 @@ impl Drk { .exec_sql( &query, rusqlite::params![ - dao.id, + dao.name, serialize_async(&proposal.recipient).await, serialize_async(&proposal.amount).await, serialize_async(&proposal.token_id).await, @@ -1070,42 +1022,21 @@ impl Drk { /// Import given DAO params into the wallet with a given name. pub async fn import_dao(&self, name: &str, params: DaoParams) -> Result<()> { // First let's check if we've imported this DAO with the given name before. - if let Ok(dao) = self.get_dao_by_name(name).await { - return Err(Error::RusqliteError(format!( - "[import_dao] This DAO has already been imported with ID {}", - dao.id - ))) + if self.get_dao_by_name(name).await.is_ok() { + return Err(Error::RusqliteError( + "[import_dao] This DAO has already been imported".to_string(), + )) } println!("Importing \"{name}\" DAO into the wallet"); let query = format!( - "INSERT INTO {} ({}, {}, {}, {}, {}, {}, {}, {}) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);", - *DAO_DAOS_TABLE, - DAO_DAOS_COL_NAME, - DAO_DAOS_COL_PROPOSER_LIMIT, - DAO_DAOS_COL_QUORUM, - DAO_DAOS_COL_APPROVAL_RATIO_BASE, - DAO_DAOS_COL_APPROVAL_RATIO_QUOT, - DAO_DAOS_COL_GOV_TOKEN_ID, - DAO_DAOS_COL_SECRET, - DAO_DAOS_COL_BULLA_BLIND, + "INSERT INTO {} ({}, {}) VALUES (?1, ?2);", + *DAO_DAOS_TABLE, DAO_DAOS_COL_NAME, DAO_DAOS_COL_PARAMS, ); if let Err(e) = self .wallet - .exec_sql( - &query, - rusqlite::params![ - name, - serialize_async(¶ms.proposer_limit).await, - serialize_async(¶ms.quorum).await, - params.approval_ratio_base, - params.approval_ratio_quot, - serialize_async(¶ms.gov_token_id).await, - serialize_async(¶ms.secret_key).await, - serialize_async(¶ms.bulla_blind).await, - ], - ) + .exec_sql(&query, rusqlite::params![name, serialize_async(¶ms).await,]) .await { return Err(Error::RusqliteError(format!("[import_dao] DAO insert failed: {e:?}"))) @@ -1114,59 +1045,8 @@ impl Drk { Ok(()) } - /// Retrieve DAO ID using provided alias filter. - pub async fn get_dao_id_by_alias(&self, alias_filter: &str) -> Result { - let row = match self - .wallet - .query_single( - &DAO_DAOS_TABLE, - &[DAO_DAOS_COL_DAO_ID], - convert_named_params! {(DAO_DAOS_COL_NAME, alias_filter)}, - ) - .await - { - Ok(r) => r, - Err(e) => { - return Err(Error::RusqliteError(format!( - "[get_dao_id_by_alias] DAO retrieval failed: {e:?}" - ))) - } - }; - - let Value::Integer(dao_id) = row[0] else { - return Err(Error::ParseFailed("[get_dao_id_by_alias] Key ID parsing failed")) - }; - let Ok(dao_id) = u64::try_from(dao_id) else { - return Err(Error::ParseFailed("[get_dao_id_by_alias] Key ID parsing failed")) - }; - - Ok(dao_id) - } - - /// Convenience function. Interprets the alias either as the DAO alias or its ID. - pub async fn get_dao_id(&self, alias: &str) -> Result { - if let Ok(id) = self.get_dao_id_by_alias(alias).await { - return Ok(id) - } - Ok(alias.parse()?) - } - - /// Fetch a DAO given a numeric ID. - pub async fn get_dao_by_id(&self, dao_id: u64) -> Result { - // TODO: instead of getting all DAOs and filtering in rust, - // we can use the DB api directly to query for the record - // and then parse it - let daos = self.get_daos().await?; - - let Some(dao) = daos.iter().find(|x| x.id == dao_id) else { - return Err(Error::RusqliteError("[get_dao_by_id] DAO not found in wallet".to_string())) - }; - - Ok(dao.clone()) - } - /// Fetch a DAO given its name. - pub async fn get_dao_by_name(&self, name: &str) -> Result { + pub async fn get_dao_by_name(&self, name: &str) -> Result { let row = match self .wallet .query_single(&DAO_DAOS_TABLE, &[], convert_named_params! {(DAO_DAOS_COL_NAME, name)}) @@ -1193,22 +1073,16 @@ impl Drk { } let daos = self.get_daos().await?; - for dao in daos { - println!("[{}] {}", dao.id, dao.name); + for (i, dao) in daos.iter().enumerate() { + println!("{i}. {}", dao.name); } Ok(()) } - /// Fetch known unspent balances from the wallet for the given DAO ID - pub async fn dao_balance(&self, dao_id: u64) -> Result> { - // TODO: instead of getting all DAOs and filtering in rust, - // we can use the DB api directly to query for the record - // and then parse it - let daos = self.get_daos().await?; - let Some(dao) = daos.get(dao_id as usize - 1) else { - return Err(Error::RusqliteError(format!("DAO with ID {dao_id} not found in wallet"))) - }; + /// Fetch known unspent balances from the wallet for the given DAO name. + pub async fn dao_balance(&self, name: &str) -> Result> { + let dao = self.get_dao_by_name(name).await?; let dao_spend_hook = FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 } @@ -1254,14 +1128,11 @@ impl Drk { } }; - // Parse DAO ID to grab the DAO record - let Value::Integer(dao_id) = row[1] else { - return Err(Error::ParseFailed("[get_dao_proposal_by_id] DAO ID parsing failed")) + // Parse DAO name to grab the DAO record + let Value::Text(ref dao_name) = row[1] else { + return Err(Error::ParseFailed("[get_dao_proposal_by_id] DAO name parsing failed")) }; - let Ok(dao_id) = u64::try_from(dao_id) else { - return Err(Error::ParseFailed("[get_dao_proposal_by_id] DAO ID parsing failed")) - }; - let dao = self.get_dao_by_id(dao_id).await?; + let dao = self.get_dao_by_name(dao_name).await?; // Parse rest of the record self.parse_dao_proposal(&dao, &row).await @@ -1375,49 +1246,81 @@ impl Drk { Ok(votes) } - /// Mint a DAO on-chain - pub async fn dao_mint(&self, dao_id: u64) -> Result { - let dao = self.get_dao_by_id(dao_id).await?; + /// Mint a DAO on-chain. + pub async fn dao_mint(&self, name: &str) -> Result { + // Retrieve the dao record + let dao = self.get_dao_by_name(name).await?; + // Check its not already minted if dao.tx_hash.is_some() { return Err(Error::Custom( "[dao_mint] This DAO seems to have already been minted on-chain".to_string(), )) } - // TODO: Simplify this model struct import once - // we use the structs from contract everwhere - let dao_info = darkfi_dao_contract::model::Dao { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_base: dao.approval_ratio_base, - approval_ratio_quot: dao.approval_ratio_quot, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, + // Now we need to do a lookup for the zkas proof bincodes, and create + // the circuit objects and proving keys so we can build the transaction. + // We also do this through the RPC. First we grab the fee call from money. + let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?; + + let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1) + else { + return Err(Error::Custom("Fee circuit not found".to_string())) }; + let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?; + + let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin); + + // Creating Fee circuit proving key + let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit); + + // Now we grab the DAO mint let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; + let Some(dao_mint_zkbin) = zkas_bins.iter().find(|x| x.0 == DAO_CONTRACT_ZKAS_DAO_MINT_NS) else { return Err(Error::RusqliteError("[dao_mint] DAO Mint circuit not found".to_string())) }; let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?; + let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin); - println!("Creating DAO Mint proving key"); + + // Creating DAO Mint circuit proving key let dao_mint_pk = ProvingKey::build(dao_mint_zkbin.k, &dao_mint_circuit); + // Create the DAO mint call let (params, proofs) = - make_mint_call(&dao_info, &dao.secret_key, &dao_mint_zkbin, &dao_mint_pk)?; - + make_mint_call(&dao.params.dao, &dao.params.secret_key, &dao_mint_zkbin, &dao_mint_pk)?; let mut data = vec![DaoFunction::Mint as u8]; params.encode_async(&mut data).await?; let call = ContractCall { contract_id: *DAO_CONTRACT_ID, data }; + + // Create the TransactionBuilder containing above call let mut tx_builder = TransactionBuilder::new(ContractCallLeaf { call, proofs }, vec![])?; + + // We first have to execute the fee-less tx to gather its used gas, and then we feed + // it into the fee-creating function. let mut tx = tx_builder.build()?; - let sigs = tx.create_sigs(&[dao.secret_key])?; - tx.signatures = vec![sigs]; + let sigs = tx.create_sigs(&[dao.params.secret_key])?; + tx.signatures.push(sigs); + + let tree = self.get_money_tree().await?; + let secret = self.default_secret().await?; + let fee_public = PublicKey::from_secret(secret); + let (fee_call, fee_proofs, fee_secrets) = + self.append_fee_call(&tx, fee_public, &tree, &fee_pk, &fee_zkbin, None).await?; + + // Append the fee call to the transaction + tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?; + + // Now build the actual transaction and sign it with all necessary keys. + let mut tx = tx_builder.build()?; + let sigs = tx.create_sigs(&[dao.params.secret_key])?; + tx.signatures.push(sigs); + let sigs = tx.create_sigs(&fee_secrets)?; + tx.signatures.push(sigs); Ok(tx) } @@ -1425,15 +1328,12 @@ impl Drk { /// Create a DAO proposal pub async fn dao_propose( &self, - dao_id: u64, + name: &str, _recipient: PublicKey, amount: u64, token_id: TokenId, ) -> Result { - let Ok(dao) = self.get_dao_by_id(dao_id).await else { - return Err(Error::RusqliteError("[dao_propose] DAO not found in wallet".to_string())) - }; - + let dao = self.get_dao_by_name(name).await?; if dao.leaf_position.is_none() || dao.tx_hash.is_none() { return Err(Error::Custom( "[dao_propose] DAO seems to not have been deployed yet".to_string(), @@ -1455,7 +1355,7 @@ impl Drk { }); let mut gov_owncoins: Vec = owncoins.iter().map(|x| x.0.clone()).collect(); - gov_owncoins.retain(|x| x.note.token_id == dao.gov_token_id); + gov_owncoins.retain(|x| x.note.token_id == dao.params.dao.gov_token_id); if dao_owncoins.is_empty() { return Err(Error::Custom(format!( @@ -1466,7 +1366,7 @@ impl Drk { if gov_owncoins.is_empty() { return Err(Error::Custom(format!( "[dao_propose] Did not find any governance {} coins in wallet", - dao.gov_token_id + dao.params.dao.gov_token_id ))) } @@ -1477,20 +1377,21 @@ impl Drk { ))) } - if gov_owncoins.iter().map(|x| x.note.value).sum::() < dao.proposer_limit { + if gov_owncoins.iter().map(|x| x.note.value).sum::() < dao.params.dao.proposer_limit { return Err(Error::Custom(format!( "[dao_propose] Not enough gov token {} balance to propose", - dao.gov_token_id + dao.params.dao.gov_token_id ))) } // FIXME: Here we're looking for a coin == proposer_limit but this shouldn't have to // be the case { - let Some(gov_coin) = gov_owncoins.iter().find(|x| x.note.value == dao.proposer_limit) + let Some(gov_coin) = + gov_owncoins.iter().find(|x| x.note.value == dao.params.dao.proposer_limit) else { return Err(Error::Custom(format!( "[dao_propose] Did not find a single gov coin of value {}", - dao.proposer_limit + dao.params.dao.proposer_limit ))) }; // } @@ -1594,22 +1495,10 @@ impl Drk { blind: Blind::random(&mut OsRng), }; - // TODO: Simplify this model struct import once - // we use the structs from contract everwhere - let daoinfo = darkfi_dao_contract::model::Dao { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_quot: dao.approval_ratio_quot, - approval_ratio_base: dao.approval_ratio_base, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, - }; - let call = DaoProposeCall { inputs: vec![input], proposal, - dao: daoinfo, + dao: dao.params.dao, dao_leaf_position: dao.leaf_position.unwrap(), dao_merkle_path, dao_merkle_root, @@ -1637,13 +1526,13 @@ impl Drk { /// Vote on a DAO proposal pub async fn dao_vote( &self, - dao_id: u64, + name: &str, proposal_id: u64, vote_option: bool, weight: u64, ) -> Result { - let dao = self.get_dao_by_id(dao_id).await?; - let proposals = self.get_dao_proposals(dao_id).await?; + let dao = self.get_dao_by_name(name).await?; + let proposals = self.get_dao_proposals(name).await?; let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else { return Err(Error::Custom("[dao_vote] Proposal ID not found".to_string())) }; @@ -1653,7 +1542,7 @@ impl Drk { let mut coins: Vec = self.get_coins(false).await?.iter().map(|x| x.0.clone()).collect(); - coins.retain(|x| x.note.token_id == dao.gov_token_id); + coins.retain(|x| x.note.token_id == dao.params.dao.gov_token_id); coins.retain(|x| x.note.spend_hook == FuncId::none()); if coins.iter().map(|x| x.note.value).sum::() < weight { @@ -1693,7 +1582,7 @@ impl Drk { } // We use the DAO secret to encrypt the vote. - let dao_keypair = Keypair::new(dao.secret_key); + let dao_keypair = dao.keypair(); // TODO: Fix this // TODO: Simplify this model struct import once @@ -1707,18 +1596,6 @@ impl Drk { blind: Blind::random(&mut OsRng), }; - // TODO: Simplify this model struct import once - // we use the structs from contract everwhere - let dao_info = darkfi_dao_contract::model::Dao { - proposer_limit: dao.proposer_limit, - quorum: dao.quorum, - approval_ratio_quot: dao.approval_ratio_quot, - approval_ratio_base: dao.approval_ratio_base, - gov_token_id: dao.gov_token_id, - public_key: PublicKey::from_secret(dao.secret_key), - bulla_blind: dao.bulla_blind, - }; - // TODO: get current height to calculate day let hasher = PoseidonFp::new(); let store = MemoryStorageFp::new(); @@ -1731,7 +1608,7 @@ impl Drk { dao_keypair, proposal, money_null_smt: &money_null_smt, - dao: dao_info, + dao: dao.params.dao, }; let zkas_bins = self.lookup_zkas(&DAO_CONTRACT_ID).await?; @@ -1780,7 +1657,7 @@ impl Drk { /// Import given DAO votes into the wallet /// This function is really bad but I'm also really tired and annoyed. - pub async fn dao_exec(&self, _dao: Dao, _proposal: DaoProposal) -> Result { + pub async fn dao_exec(&self, _dao: DaoRecord, _proposal: DaoProposal) -> Result { // TODO unimplemented!() } diff --git a/bin/drk/src/main.rs b/bin/drk/src/main.rs index ca20fe3bb..ec7324a06 100644 --- a/bin/drk/src/main.rs +++ b/bin/drk/src/main.rs @@ -335,20 +335,20 @@ enum DaoSubcmd { /// Show the balance of a DAO Balance { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, }, /// Mint an imported DAO on-chain Mint { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, }, /// Create a proposal for a DAO Propose { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, /// Pubkey to send tokens to with proposal success recipient: String, @@ -362,14 +362,14 @@ enum DaoSubcmd { /// List DAO proposals Proposals { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, }, /// View a DAO proposal data Proposal { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, /// Numeric identifier for the proposal proposal_id: u64, @@ -377,8 +377,8 @@ enum DaoSubcmd { /// Vote on a given proposal Vote { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, /// Numeric identifier for the proposal proposal_id: u64, @@ -392,8 +392,8 @@ enum DaoSubcmd { /// Execute a DAO proposal Exec { - /// Name or numeric identifier for the DAO - dao_alias: String, + /// Name identifier for the DAO + name: String, /// Numeric identifier for the proposal proposal_id: u64, @@ -1039,9 +1039,9 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { }; let secret_key = SecretKey::random(&mut OsRng); - let bulla_blind = pallas::Base::random(&mut OsRng); + let bulla_blind = BaseBlind::random(&mut OsRng); - let params = DaoParams { + let params = DaoParams::new( proposer_limit, quorum, approval_ratio_base, @@ -1049,7 +1049,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { gov_token_id, secret_key, bulla_blind, - }; + ); let encoded = bs58::encode(&serialize_async(¶ms).await).into_string(); println!("{encoded}"); @@ -1074,7 +1074,6 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { let params: DaoParams = deserialize_async(&bytes).await?; let drk = Drk::new(args.wallet_path, args.wallet_pass, None, ex).await?; - if let Err(e) = drk.import_dao(&name, params).await { eprintln!("Failed to import DAO: {e:?}"); exit(2); @@ -1093,11 +1092,9 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Balance { dao_alias } => { + DaoSubcmd::Balance { name } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, None, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let balmap = match drk.dao_balance(dao_id).await { + let balmap = match drk.dao_balance(&name).await { Ok(b) => b, Err(e) => { eprintln!("Failed to fetch DAO balance: {e:?}"); @@ -1139,23 +1136,22 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Mint { dao_alias } => { + DaoSubcmd::Mint { name } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let tx = match drk.dao_mint(dao_id).await { + let tx = match drk.dao_mint(&name).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to mint DAO: {e:?}"); exit(2); } }; + println!("{}", base64::encode(&serialize_async(&tx).await)); Ok(()) } - DaoSubcmd::Propose { dao_alias, recipient, amount, token } => { + DaoSubcmd::Propose { name, recipient, amount, token } => { if let Err(e) = f64::from_str(&amount) { eprintln!("Invalid amount: {e:?}"); exit(2); @@ -1171,7 +1167,6 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; let token_id = match drk.get_token(token).await { Ok(t) => t, Err(e) => { @@ -1180,7 +1175,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { } }; - let tx = match drk.dao_propose(dao_id, rcpt, amount, token_id).await { + let tx = match drk.dao_propose(&name, rcpt, amount, token_id).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create DAO proposal: {e:?}"); @@ -1191,11 +1186,9 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Proposals { dao_alias } => { + DaoSubcmd::Proposals { name } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, None, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let proposals = drk.get_dao_proposals(dao_id).await?; + let proposals = drk.get_dao_proposals(&name).await?; for proposal in proposals { println!("[{}] {:?}", proposal.id, proposal.bulla()); @@ -1204,11 +1197,9 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Proposal { dao_alias, proposal_id } => { + DaoSubcmd::Proposal { name, proposal_id } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, None, ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - - let proposals = drk.get_dao_proposals(dao_id).await?; + let proposals = drk.get_dao_proposals(&name).await?; let Some(proposal) = proposals.iter().find(|x| x.id == proposal_id) else { eprintln!("No such DAO proposal found"); exit(2); @@ -1226,11 +1217,7 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Vote { dao_alias, proposal_id, vote, vote_weight } => { - let drk = - Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - + DaoSubcmd::Vote { name, proposal_id, vote, vote_weight } => { if let Err(e) = f64::from_str(&vote_weight) { eprintln!("Invalid vote weight: {e:?}"); exit(2); @@ -1243,7 +1230,9 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { } let vote = vote != 0; - let tx = match drk.dao_vote(dao_id, proposal_id, vote, weight).await { + let drk = + Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; + let tx = match drk.dao_vote(&name, proposal_id, vote, weight).await { Ok(tx) => tx, Err(e) => { eprintln!("Failed to create DAO Vote transaction: {e:?}"); @@ -1258,11 +1247,10 @@ async fn realmain(args: Args, ex: Arc>) -> Result<()> { Ok(()) } - DaoSubcmd::Exec { dao_alias, proposal_id } => { + DaoSubcmd::Exec { name, proposal_id } => { let drk = Drk::new(args.wallet_path, args.wallet_pass, Some(args.endpoint), ex).await?; - let dao_id = drk.get_dao_id(&dao_alias).await?; - let dao = drk.get_dao_by_id(dao_id).await?; + let dao = drk.get_dao_by_name(&name).await?; let proposal = drk.get_dao_proposal_by_id(proposal_id).await?; assert!(proposal.dao_bulla == dao.bulla()); diff --git a/contrib/localnet/darkfid-single-node/README.md b/contrib/localnet/darkfid-single-node/README.md index 0aeb28851..6bc481dc8 100644 --- a/contrib/localnet/darkfid-single-node/README.md +++ b/contrib/localnet/darkfid-single-node/README.md @@ -65,7 +65,7 @@ of the guide can be added for future regressions. | 20 | DAO view | dao view | Pass | | 21 | DAO import | dao import | Pass | | 22 | DAO list | dao list | Pass | -| 23 | DAO mint | dao mint {DAO} | Failure: needs #19 | +| 23 | DAO mint | dao mint {DAO} | Pass | | 24 | DAO balance | dao balance {DAO} | Failure: needs #19 | | 25 | DAO propose | dao propose {DAO} {ADDR} {AMOUNT} {TOKEN} | Failure: needs #19 | | 26 | DAO proposals retrieval | dao proposals {DAO} | Failure: needs #25 | diff --git a/src/contract/dao/src/model.rs b/src/contract/dao/src/model.rs index cd0ae1fed..5dbb3f837 100644 --- a/src/contract/dao/src/model.rs +++ b/src/contract/dao/src/model.rs @@ -37,12 +37,18 @@ use darkfi_serial::async_trait; // ANCHOR: dao /// DAOs are represented on chain as a commitment to this object pub struct Dao { + /// The minimum amount of governance tokens needed to open a proposal pub proposer_limit: u64, + /// Minimal threshold of participating total tokens needed for a proposal to pass pub quorum: u64, + /// The ratio of winning/total votes needed for a proposal to pass pub approval_ratio_quot: u64, pub approval_ratio_base: u64, + /// DAO's governance token ID pub gov_token_id: TokenId, + /// Public key of the DAO pub public_key: PublicKey, + /// DAO bulla blind pub bulla_blind: BaseBlind, } // ANCHOR_END: dao