diff --git a/.gitignore b/.gitignore index 93684ba..be44988 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,6 @@ cache/ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk +client/tmp/ .DS_Store \ No newline at end of file diff --git a/client/Cargo.toml b/client/Cargo.toml index 73f6d2b..94a8efb 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,27 +9,31 @@ readme = "README.md" description = "Submit extrinsics (transactions) to the sunshine node via RPC" keywords = ["sunshine", "substrate", "blockchain"] +[features] +light-client = ["substrate-subxt-light-client", "sunshine-node"] + [dependencies] async-std = "=1.5.0" ipld-block-builder = "0.3.0" libipld = { version = "0.3.0", features = ["dag-json"] } serde = { version = "1.0.111", features = ["derive"] } -# TODO export error in libipld sled = "0.32.0-rc1" serde_json = "1.0.53" ipfs-embed = "0.1.0" -# point at git master, could be done with patch.crates.io = giturl substrate-subxt = "0.8.0" surf = "1.0.3" thiserror = "1.0.19" -utils-identity = { version = "0.1.0", path = "../utils" } -# substrate sp-runtime = { version = "2.0.0-rc3", default-features = false } sp-core = { version = "2.0.0-rc3", default-features = false } codec = { version = "1.3.0", package = "parity-scale-codec" } frame-support = "2.0.0-rc3" pallet-indices = "2.0.0-rc3" +derive-new = {version = "0.5", default-features=false} +substrate-subxt-light-client = { package = "substrate-subxt", git = "https://github.com/dvc94ch/substrate-subxt", branch = "lightclient", optional = true } +sunshine-node = { path = "../node", default-features = false, optional = true } +keystore = {package = "keybase-keystore", git = "https://github.com/sunshine-protocol/substrate-identity"} # local deps +utils-identity = { version = "0.1.0", path = "../utils" } util = { package = "sunshine-util", path = "../modules/util", default-features = false } org = {package = "sunshine-org", path = "../modules/org", default-features=false } vote = { package = "sunshine-vote", path = "../modules/vote", default-features=false} diff --git a/client/examples/org.rs b/client/examples/org.rs new file mode 100644 index 0000000..21dd560 --- /dev/null +++ b/client/examples/org.rs @@ -0,0 +1,42 @@ +use sp_keyring::AccountKeyring; +//#[cfg(feature = "light-client")] +//use sunshine_client::ChainType; +use ipfs_embed::{Config, Store}; +use ipld_block_builder::{BlockBuilder, Codec}; +use keystore::{DeviceKey, KeyStore, Password}; +use sp_core::crypto::Pair; +use sunshine_client::{ClientBuilder, Error, Runtime, SunClient}; +// use libipld::cid::{Cid, Codec}; +// use libipld::multihash::Sha2_256; +// use utils_identity::cid::CidBytes; + +#[async_std::main] +async fn main() -> Result<(), Error> { + env_logger::init(); + let subxt = ClientBuilder::new().build().await.unwrap(); + let db = sled::open("tmp/db")?; + let ipld_tree = db.open_tree("ipld_tree")?; + let config = Config::from_tree(ipld_tree); + let store = Store::new(config)?; + let codec = Codec::new(); + let ipld = BlockBuilder::new(store, codec); + let keystore = KeyStore::new("/tmp/keystore"); + let alice_seed: [u8; 32] = AccountKeyring::Alice.into(); + let _ = keystore.initialize( + &DeviceKey::from_seed(alice_seed), + &Password::from("password".to_string()), + )?; + // //#[cfg(not(feature = "light-client"))] + let client = SunClient::new(subxt, keystore, ipld); + // #[cfg(feature = "light-client")] + // let client = Sunshine::new("/tmp/db", signer, ChainType::Development).await?; + let account_id = sp_keyring::AccountKeyring::Alice.to_account_id(); + client.issue_shares(1u64, account_id, 10u64).await?; + + // println!( + // "Account {:?} was issued {:?} shares for organization {:?}", + // event.who, event.shares, event.organization, + // ); + + Ok(()) +} diff --git a/client/examples/reserve_shares_and_watch.rs b/client/examples/reserve_shares_and_watch.rs deleted file mode 100644 index 466641c..0000000 --- a/client/examples/reserve_shares_and_watch.rs +++ /dev/null @@ -1,23 +0,0 @@ -use sp_keyring::AccountKeyring; -#[cfg(feature = "light-client")] -use sunshine_client::ChainType; -use sunshine_client::{Error, Sunshine}; - -#[async_std::main] -async fn main() -> Result<(), Error> { - env_logger::init(); - let signer = AccountKeyring::Alice.pair(); - #[cfg(not(feature = "light-client"))] - let client = Sunshine::new("/tmp/db", signer).await?; - #[cfg(feature = "light-client")] - let client = Sunshine::new("/tmp/db", signer, ChainType::Development).await?; - - let event = client.reserve_shares(1, 1).await?; - - println!( - "Account {:?} reserved {:?} shares with share id {:?} for organization {:?}", - event.account, event.reserved, event.share, event.org, - ); - - Ok(()) -} diff --git a/client/src/error.rs b/client/src/error.rs index 68670c1..5927190 100644 --- a/client/src/error.rs +++ b/client/src/error.rs @@ -11,6 +11,12 @@ pub enum Error { Sled(#[from] sled::Error), #[error("{0}")] Ipfs(#[from] ipfs_embed::Error), + #[error(transparent)] + Keystore(#[from] keystore::Error), + #[error("keystore already initialized")] + KeystoreInitialized, #[error("event not found")] EventNotFound, } + +pub type Result = core::result::Result; diff --git a/client/src/lib.rs b/client/src/lib.rs index a6e263c..243fdc5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -3,6 +3,9 @@ #[macro_use] extern crate substrate_subxt; +#[macro_use] +extern crate derive_new; + mod error; #[cfg(feature = "light-client")] mod light_client; @@ -13,4 +16,5 @@ mod sunshine; pub use error::Error; #[cfg(feature = "light-client")] pub use light_client::ChainType; -pub use sunshine::Sunshine; +pub use runtime::{ClientBuilder, Runtime}; +pub use sunshine::SunClient; diff --git a/client/src/runtime.rs b/client/src/runtime.rs index 1445e1d..28da299 100644 --- a/client/src/runtime.rs +++ b/client/src/runtime.rs @@ -10,15 +10,21 @@ use sp_runtime::{ use std::marker::PhantomData; use substrate_subxt::{ balances::{AccountData, Balances}, - system::System, - CheckEra, CheckGenesis, CheckNonce, CheckSpecVersion, CheckTxVersion, CheckWeight, SignedExtra, + system::System, //sp_core::crypto::Pair, + CheckEra, + CheckGenesis, + CheckNonce, + CheckSpecVersion, + CheckTxVersion, + CheckWeight, + SignedExtra, }; use utils_identity::cid::CidBytes; pub type Pair = sp_core::sr25519::Pair; pub type ClientBuilder = substrate_subxt::ClientBuilder; pub type Client = substrate_subxt::Client; -//pub type XtBuilder = substrate_subxt::XtBuilder; +pub type PairSigner = substrate_subxt::PairSigner; /// Concrete type definitions compatible w/ sunshine's runtime aka `suntime` #[derive(Debug, Clone, Eq, PartialEq)] @@ -30,7 +36,7 @@ impl System for Runtime { type Hash = sp_core::H256; type Hashing = BlakeTwo256; type AccountId = <::Signer as IdentifyAccount>::AccountId; - type Address = pallet_indices::address::Address; + type Address = pallet_indices::address::Address; type Header = Header; type Extrinsic = OpaqueExtrinsic; type AccountData = AccountData<::Balance>; diff --git a/client/src/srml/bank.rs b/client/src/srml/bank.rs new file mode 100644 index 0000000..7b8a497 --- /dev/null +++ b/client/src/srml/bank.rs @@ -0,0 +1,224 @@ +use crate::srml::{ + org::{Org, OrgEventsDecoder}, + vote::{Vote, VoteEventsDecoder}, +}; +use codec::{Codec, Decode, Encode}; +use frame_support::Parameter; +use sp_runtime::traits::{AtLeast32Bit, MaybeSerializeDeserialize, Member, Zero}; +use std::fmt::Debug; +use substrate_subxt::system::{System, SystemEventsDecoder}; +use util::bank::{ + BankState, DepositInfo, InternalTransferInfo, OnChainTreasuryID, ReservationInfo, +}; + +pub type BalanceOf = ::Currency; // as Currency<::AccountId>>::Balance; + +/// The subset of the bank trait and its inherited traits that the client must inherit +#[module] +pub trait Bank: System + Org + Vote { + /// Identifier for bank-related maps + type BankAssociatedId: Parameter + + Member + + AtLeast32Bit + + Codec + + Default + + Copy + + MaybeSerializeDeserialize + + Debug + + PartialOrd + + PartialEq + + Zero; + + /// The currency type for on-chain transactions + type Currency: Parameter + + Member + + AtLeast32Bit + + Codec + + Default + + Copy + + MaybeSerializeDeserialize + + Debug + + PartialOrd + + PartialEq + + Zero; // + Currency<::AccountId> // commented out until #93 is resolved +} + +// ~~ Values (Constants) ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Encode)] +pub struct MinimumInitialDepositStore { + pub amount: BalanceOf, +} + +// ~~ Maps ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct BankStoresStore { + #[store(returns = BankState<::OrgId, BalanceOf>)] + pub id: OnChainTreasuryID, + phantom: std::marker::PhantomData, +} + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct DepositsStore { + #[store(returns = DepositInfo<::AccountId, ::IpfsReference, BalanceOf>)] + pub bank_id: OnChainTreasuryID, + pub deposit_id: T::BankAssociatedId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct SpendReservationsStore { + #[store(returns = ReservationInfo<::IpfsReference, BalanceOf, ::OrgId>)] + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct InternalTransfersStore { + #[store(returns = InternalTransferInfo::IpfsReference, BalanceOf, ::OrgId>)] + pub bank_id: OnChainTreasuryID, + pub transfer_id: T::BankAssociatedId, +} + +// ~~ (Calls, Events) ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DepositFromSignerForBankAccountCall { + pub bank_id: OnChainTreasuryID, + pub amount: BalanceOf, + pub reason: ::IpfsReference, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct CapitalDepositedIntoOnChainBankAccountEvent { + pub depositer: ::AccountId, + pub bank_id: OnChainTreasuryID, + pub amount: BalanceOf, + pub reason: ::IpfsReference, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct RegisterAndSeedForBankAccountCall { + pub seed: BalanceOf, + pub hosting_org: ::OrgId, + pub bank_operator: Option<::OrgId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct RegisteredNewOnChainBankEvent { + pub seeder: ::AccountId, + pub new_bank_id: OnChainTreasuryID, + pub seed: BalanceOf, + pub hosting_org: ::OrgId, + pub bank_operator: Option<::OrgId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct ReserveSpendForBankAccountCall { + pub bank_id: OnChainTreasuryID, + pub reason: ::IpfsReference, + pub amount: BalanceOf, + pub controller: ::OrgId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct SpendReservedForBankAccountEvent { + pub bank_id: OnChainTreasuryID, + pub new_reservation_id: T::BankAssociatedId, + pub reason: ::IpfsReference, + pub amount: BalanceOf, + pub controller: ::OrgId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct CommitReserveSpendForTransferInsideBankAccountCall { + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, + pub reason: ::IpfsReference, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct CommitSpendBeforeInternalTransferEvent { + pub committer: ::AccountId, + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct UnreserveUncommittedReservationToMakeFreeCall { + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct UnreserveUncommittedReservationToMakeFreeEvent { + pub qualified_bank_controller: ::AccountId, + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct UnreserveCommittedReservationToMakeFreeCall { + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct UnreserveCommittedReservationToMakeFreeEvent { + pub qualified_spend_reservation_controller: ::AccountId, + pub bank_id: OnChainTreasuryID, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct TransferSpendingPowerForSpendCommitmentCall { + pub bank_id: OnChainTreasuryID, + pub reason: ::IpfsReference, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, + pub committed_controller: ::OrgId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct InternalTransferExecutedAndSpendingPowerDoledOutToControllerEvent { + pub qualified_spend_reservation_controller: ::AccountId, + pub bank_id: OnChainTreasuryID, + pub reason: ::IpfsReference, + pub reservation_id: T::BankAssociatedId, + pub amount: BalanceOf, + pub committed_controller: ::OrgId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct WithdrawByReferencingInternalTransferCall { + pub bank_id: OnChainTreasuryID, + pub transfer_id: T::BankAssociatedId, + pub amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct SpendRequestForInternalTransferApprovedAndExecutedEvent { + pub bank_id: OnChainTreasuryID, + pub requester: ::AccountId, + pub amount: BalanceOf, + pub transfer_id: T::BankAssociatedId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct BurnAllSharesToLeaveWeightedMembershipBankAndWithdrawRelatedFreeCapitalCall { + pub bank_id: OnChainTreasuryID, + phantom: std::marker::PhantomData, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct AccountLeftMembershipAndWithdrewProportionOfFreeCapitalInBankEvent { + pub bank_id: OnChainTreasuryID, + pub leaving_member: ::AccountId, + pub amount_withdrawn_by_burning_shares: BalanceOf, +} diff --git a/client/src/srml/bounty.rs b/client/src/srml/bounty.rs new file mode 100644 index 0000000..6ccb7de --- /dev/null +++ b/client/src/srml/bounty.rs @@ -0,0 +1,250 @@ +use crate::srml::{ + bank::{BalanceOf, Bank, BankEventsDecoder}, + org::{Org, OrgEventsDecoder}, + vote::{Vote, VoteEventsDecoder}, +}; +use codec::{Codec, Decode, Encode}; +use frame_support::Parameter; +use sp_runtime::{ + traits::{AtLeast32Bit, MaybeSerializeDeserialize, Member, Zero}, + Permill, +}; +use std::fmt::Debug; +use substrate_subxt::system::{System, SystemEventsDecoder}; +use util::{ + bank::OnChainTreasuryID, + bounty::{ + ApplicationState, BountyInformation, GrantApplication, MilestoneStatus, + MilestoneSubmission, ReviewBoard, TeamID, + }, + organization::TermsOfAgreement, + vote::ThresholdConfig, +}; + +#[module] +pub trait Bounty: System + Org + Vote + Bank { + /// Identifier for bounty-related maps and submaps + type BountyId: Parameter + + Member + + AtLeast32Bit + + Codec + + Default + + Copy + + MaybeSerializeDeserialize + + Debug + + PartialOrd + + PartialEq + + Zero; +} + +// ~~ Constants ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Encode)] +pub struct MinimumBountyCollateralRatioConstant { + pub get: Permill, +} + +#[derive(Clone, Debug, Eq, PartialEq, Encode)] +pub struct BountyLowerBoundConstant { + pub get: BalanceOf, +} + +// ~~ Maps ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct RegisteredFoundationsStore { + #[store(returns = bool)] + pub org: ::OrgId, + pub treasury_id: OnChainTreasuryID, +} + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct FoundationSponsoredBountiesStore { + #[store(returns = BountyInformation<::OrgId, ::BankAssociatedId, ::IpfsReference, BalanceOf, ReviewBoard<::OrgId, ::AccountId, ::IpfsReference, ThresholdConfig<::Signal, Permill>>>)] + pub id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct BountyApplicationsStore { + #[store(returns = GrantApplication<::AccountId, ::Shares, BalanceOf, ::IpfsReference, ApplicationState::OrgId, ::AccountId>, ::VoteId>>)] + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct MilestoneSubmissionsStore { + #[store(returns = MilestoneSubmission<::IpfsReference, BalanceOf, ::AccountId, T::BountyId, MilestoneStatus<::OrgId, ::VoteId, ::BankAssociatedId>>)] + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, +} + +// ~~ (Calls, Events) ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectRegisterFoundationFromExistingBankCall { + pub registered_organization: ::OrgId, + pub bank_account: OnChainTreasuryID, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct FoundationRegisteredFromOnChainBankEvent { + pub registered_organization: ::OrgId, + pub bank_account: OnChainTreasuryID, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectCreateBountyCall { + pub registered_organization: ::OrgId, + pub description: ::IpfsReference, + pub bank_account: OnChainTreasuryID, + pub amount_reserved_for_bounty: BalanceOf, + pub amount_claimed_available: BalanceOf, + pub acceptance_committee: ReviewBoard< + ::OrgId, + ::AccountId, + ::IpfsReference, + ThresholdConfig<::Signal, Permill>, + >, + pub supervision_committee: Option< + ReviewBoard< + ::OrgId, + ::AccountId, + ::IpfsReference, + ThresholdConfig<::Signal, Permill>, + >, + >, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct FoundationPostedBountyEvent { + pub bounty_creator: ::AccountId, + pub registered_organization: ::OrgId, + pub bounty_identifier: T::BountyId, + pub bank_account: OnChainTreasuryID, + pub description: ::IpfsReference, + pub amount_reserved_for_bounty: BalanceOf, + pub amount_claimed_available: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectSubmitGrantApplicationCall { + pub bounty_id: T::BountyId, + pub description: ::IpfsReference, + pub total_amount: BalanceOf, + pub terms_of_agreement: + TermsOfAgreement<::AccountId, ::Shares, ::IpfsReference>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct GrantApplicationSubmittedForBountyEvent { + pub submitter: ::AccountId, + pub bounty_id: T::BountyId, + pub new_grant_app_id: T::BountyId, + pub description: ::IpfsReference, + pub total_amount: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectTriggerApplicationReviewCall { + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct ApplicationReviewTriggeredEvent { + pub trigger: ::AccountId, + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, + pub application_state: + ApplicationState::OrgId, ::AccountId>, ::VoteId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectSudoApproveApplicationCall { + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct SudoApprovedApplicationEvent { + pub purported_sudo: ::AccountId, + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, + pub application_state: + ApplicationState::OrgId, ::AccountId>, ::VoteId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct AnyAccPollApplicationCall { + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct ApplicationPolledEvent { + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, + pub application_state: + ApplicationState::OrgId, ::AccountId>, ::VoteId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectSubmitMilestoneCall { + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, + pub submission_reference: ::IpfsReference, + pub amount_requested: BalanceOf, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct MilestoneSubmittedEvent { + pub submitter: ::AccountId, + pub bounty_id: T::BountyId, + pub application_id: T::BountyId, + pub new_milestone_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectTriggerMilestoneReviewCall { + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct MilestoneReviewTriggeredEvent { + pub trigger: ::AccountId, + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, + pub milestone_state: + MilestoneStatus<::OrgId, ::VoteId, ::BankAssociatedId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectSudoApprovesMilestoneCall { + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct SudoApprovedMilestoneEvent { + pub purported_sudo: ::AccountId, + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, + pub milestone_state: + MilestoneStatus<::OrgId, ::VoteId, ::BankAssociatedId>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct DirectPollMilestoneCall { + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct MilestonePolledEvent { + pub poller: ::AccountId, + pub bounty_id: T::BountyId, + pub milestone_id: T::BountyId, + pub milestone_state: + MilestoneStatus<::OrgId, ::VoteId, ::BankAssociatedId>, +} diff --git a/client/src/srml/mod.rs b/client/src/srml/mod.rs index 1cf05dc..2556a1e 100644 --- a/client/src/srml/mod.rs +++ b/client/src/srml/mod.rs @@ -1,2 +1,4 @@ +pub mod bank; +pub mod bounty; pub mod org; -//pub mod vote; +pub mod vote; diff --git a/client/src/srml/org.rs b/client/src/srml/org.rs index 0135249..4b24bb7 100644 --- a/client/src/srml/org.rs +++ b/client/src/srml/org.rs @@ -38,23 +38,23 @@ pub trait Org: System { #[derive(Clone, Debug, Eq, PartialEq, Encode)] pub struct SudoKeyStore { - pub sudo: Option<::AccountId>, + pub sudo_key: Option<::AccountId>, } #[derive(Clone, Debug, Eq, PartialEq, Encode)] -pub struct OrganizationIdentifierNonceStore { - pub nonce: T::OrgId, +pub struct OrganizationIdNonceStore { + pub org_id_nonce: T::OrgId, } #[derive(Clone, Debug, Eq, PartialEq, Encode)] pub struct OrganizationCounterStore { - pub counter: u32, + pub organization_counter: u32, } // ~~ Maps ~~ #[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] -pub struct OrgStateStore { +pub struct OrganizationStatesStore { #[store(returns = Organization<::AccountId, T::OrgId, T::IpfsReference>)] pub org: T::OrgId, } @@ -66,16 +66,10 @@ pub struct TotalIssuanceStore { } #[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] -pub struct ProfileStore<'a, T: Org> { +pub struct MembersStore<'a, T: Org> { #[store(returns = ShareProfile)] pub org: T::OrgId, - pub account: &'a ::AccountId, -} - -#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] -pub struct OrganizationSupervisor { - #[store(returns = Option<::AccountId>)] - pub org: T::OrgId, + pub who: &'a ::AccountId, } #[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] @@ -84,286 +78,138 @@ pub struct OrganizationSizeStore { pub org: T::OrgId, } +// ~~ (Calls, Events) ~~ + #[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct IssueSharesCall<'a, T: Org> { - pub org: T::OrgId, - pub who: &'a ::AccountId, - pub amount: T::Shares, +pub struct RegisterFlatOrgCall<'a, T: Org> { + pub sudo: Option<::AccountId>, + pub parent_org: Option, + pub constitution: T::IpfsReference, + pub members: &'a [::AccountId], } -#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct BurnSharesCall<'a, T: Org> { - pub org: T::OrgId, - pub who: &'a ::AccountId, - pub amount: T::Shares, +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct NewFlatOrganizationRegisteredEvent { + pub caller: ::AccountId, + pub new_id: T::OrgId, + pub constitution: T::IpfsReference, + pub total: u32, } #[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct BatchIssueSharesCall<'a, T: Org> { - pub org: T::OrgId, - pub new_accounts: &'a [(::AccountId, T::Shares)], +pub struct RegisterWeightedOrgCall<'a, T: Org> { + pub sudo: Option<::AccountId>, + pub parent_org: Option, + pub constitution: T::IpfsReference, + pub weighted_members: &'a [(::AccountId, T::Shares)], } -#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct BatchBurnSharesCall<'a, T: Org> { - pub org: T::OrgId, - pub new_accounts: &'a [(::AccountId, T::Shares)], +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct NewWeightedOrganizationRegisteredEvent { + pub caller: ::AccountId, + pub new_id: T::OrgId, + pub constitution: T::IpfsReference, + pub total: T::Shares, } #[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct LockSharesCall<'a, T: Org> { - pub org: T::OrgId, +pub struct IssueSharesCall<'a, T: Org> { + pub organization: T::OrgId, pub who: &'a ::AccountId, + pub shares: T::Shares, } -#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct UnlockSharesCall<'a, T: Org> { - pub org: T::OrgId, - pub who: &'a ::AccountId, +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct SharesIssuedEvent { + pub organization: T::OrgId, + pub who: ::AccountId, + pub shares: T::Shares, } #[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct ReserveSharesCall<'a, T: Org> { - pub org: T::OrgId, +pub struct BurnSharesCall<'a, T: Org> { + pub organization: T::OrgId, pub who: &'a ::AccountId, + pub shares: T::Shares, +} + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct SharesBurnedEvent { + pub organization: T::OrgId, + pub who: ::AccountId, + pub shares: T::Shares, } #[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] -pub struct UnreserveSharesCall<'a, T: Org> { - pub org: T::OrgId, - pub who: &'a ::AccountId, +pub struct BatchIssueSharesCall<'a, T: Org> { + pub organization: T::OrgId, + pub new_accounts: &'a [(::AccountId, T::Shares)], } #[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] -pub struct SharesReservedEvent { - pub org: T::OrgId, - pub who: ::AccountId, - pub amount: T::Shares, +pub struct SharesBatchIssuedEvent { + pub organization: T::OrgId, + pub total_new_shares_minted: T::Shares, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct BatchBurnSharesCall<'a, T: Org> { + pub organization: T::OrgId, + pub old_accounts: &'a [(::AccountId, T::Shares)], } #[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] -pub struct SharesUnReservedEvent { - pub org: T::OrgId, - pub who: ::AccountId, - pub amount: T::Shares, +pub struct SharesBatchBurnedEvent { + pub organization: T::OrgId, + pub total_new_shares_burned: T::Shares, +} + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct LockSharesCall<'a, T: Org> { + pub organization: T::OrgId, + pub who: &'a ::AccountId, } #[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] pub struct SharesLockedEvent { - pub org: T::OrgId, + pub organization: T::OrgId, pub who: ::AccountId, } +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct UnlockSharesCall<'a, T: Org> { + pub organization: T::OrgId, + pub who: &'a ::AccountId, +} + #[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] pub struct SharesUnlockedEvent { - pub org: T::OrgId, + pub organization: T::OrgId, pub who: ::AccountId, } -#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] -pub struct SharesIssuedEvent { - pub org: T::OrgId, - pub account: ::AccountId, - pub amount: T::Shares, +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct ReserveSharesCall<'a, T: Org> { + pub organization: T::OrgId, + pub who: &'a ::AccountId, } #[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] -pub struct SharesBurnedEvent { - pub org: T::OrgId, - pub account: ::AccountId, - pub amount: T::Shares, +pub struct SharesReservedEvent { + pub organization: T::OrgId, + pub who: ::AccountId, + pub amount_reserved: T::Shares, } -#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] -pub struct SharesBatchIssuedEvent { - pub org: T::OrgId, - pub amount: T::Shares, +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct UnreserveSharesCall<'a, T: Org> { + pub organization: T::OrgId, + pub who: &'a ::AccountId, } #[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] -pub struct SharesBatchBurnedEvent { - pub org: T::OrgId, - pub amount: T::Shares, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::runtime::{Runtime, RuntimeExtra}; - - subxt_test!({ - name: issue_shares, - runtime: Runtime, - extra: RuntimeExtra, - /*state: { - issuance: TotalIssuanceStore { - org: 1, - share: 1, - }, - },*/ - step: { - call: IssueSharesCall { - org: 1, - share: 1, - who: &alice, - amount: 10, - }, - event: SharesIssuedEvent { - org: 1, - share: 1, - account: alice.clone(), - amount: 10, - }, - /*assert: { - assert_eq!(pre.issuance + 20, post.issuance); - },*/ - }, - step: { - call: BurnSharesCall { - org: 1, - share: 1, - who: &alice, - amount: 10, - }, - event: SharesBurnedEvent { - org: 1, - share: 1, - account: alice.clone(), - amount: 10, - }, - /*assert: { - assert_eq!(pre.issuance - 20, post.issuance); - },*/ - } - }); - - subxt_test!({ - name: batch_issue_shares, - runtime: Runtime, - extra: RuntimeExtra, - /*state: { - issuance: TotalIssuanceStore { - org: 1, - share: 1, - }, - },*/ - step: { - call: BatchIssueSharesCall { - org: 1, - share: 1, - new_accounts: &[(alice.clone(), 10), (bob.clone(), 10)], - }, - event: SharesBatchIssuedEvent { - org: 1, - share: 1, - amount: 20, - }, - /*assert: { - assert_eq!(pre.issuance + 20, post.issuance); - },*/ - }, - step: { - call: BatchBurnSharesCall { - org: 1, - share: 1, - new_accounts: &[(alice.clone(), 10), (bob.clone(), 10)], - }, - event: SharesBatchBurnedEvent { - org: 1, - share: 1, - amount: 20, - }, - /*assert: { - assert_eq!(pre.issuance - 20, post.issuance); - },*/ - } - }); - - subxt_test!({ - name: reserve_shares, - runtime: Runtime, - extra: RuntimeExtra, - /*state: { - profile: ProfileStore { - prefix: UUID2::new(1, 1), - account_id: &alice, - }, - },*/ - step: { - call: ReserveSharesCall { - org: 1, - share: 1, - who: &alice, - }, - event: SharesReservedEvent { - org: 1, - share: 1, - account: alice.clone(), - reserved: event.reserved, //pre.profile.times_reserved() + 1, - }, - /*assert: { - assert_eq!(pre.profile.times_reserved() + 1, post.profile.times_reserved()); - },*/ - }, - step: { - call: UnreserveSharesCall { - org: 1, - share: 1, - who: &alice, - }, - event: SharesUnReservedEvent { - org: 1, - share: 1, - account: alice.clone(), - reserved: event.reserved, //pre.profile.times_reserved() - 1, - }, - /*assert: { - assert_eq!(pre.profile.times_reserved() - 1, post.profile.times_reserved()); - },*/ - } - }); - - subxt_test!({ - name: lock_shares, - runtime: Runtime, - extra: RuntimeExtra, - /*state: { - profile: ProfileStore { - prefix: UUID2::new(1, 1), - account_id: &alice, - }, - },*/ - step: { - call: LockSharesCall { - org: 1, - share: 1, - who: &alice, - }, - event: SharesLockedEvent { - org: 1, - share: 1, - account: alice.clone(), - }, - /*assert: { - assert_eq!(pre.profile.is_unlocked(), true); - assert_eq!(post.profile.is_unlocked(), false); - },*/ - }, - step: { - call: UnlockSharesCall { - org: 1, - share: 1, - who: &alice, - }, - event: SharesUnlockedEvent { - org: 1, - share: 1, - account: alice.clone(), - }, - /*assert: { - assert_eq!(pre.profile.is_unlocked(), false); - assert_eq!(post.profile.is_unlocked(), true); - },*/ - } - }); +pub struct SharesUnReservedEvent { + pub organization: T::OrgId, + pub who: ::AccountId, + pub amount_unreserved: T::Shares, } diff --git a/client/src/srml/vote.rs b/client/src/srml/vote.rs index 28f0cc3..31a32da 100644 --- a/client/src/srml/vote.rs +++ b/client/src/srml/vote.rs @@ -1,22 +1,17 @@ -//! Implements support for the vote_yesno module - -use codec::Codec; +use crate::srml::org::{Org, OrgEventsDecoder}; +use codec::{Codec, Decode, Encode}; use frame_support::Parameter; use sp_runtime::{ traits::{AtLeast32Bit, MaybeSerializeDeserialize, Member, Zero}, Permill, }; use std::fmt::Debug; -use substrate_subxt::{system::System, Call}; -use substrate_subxt_proc_macro::*; -use util::{ - traits::{GroupMembership, LockableProfile, ReservableProfile, ShareBank}, - voteyesno::VoterYesNoView, -}; +use substrate_subxt::system::{System, SystemEventsDecoder}; +use util::vote::{Vote as VoteVector, VoteState, VoterView}; -/// The subset of the `vote_yesno::Trait` that a client must implement. +/// The subset of the `vote::Trait` that a client must implement. #[module] -pub trait VoteYesNo: System { +pub trait Vote: System + Org { /// The identifier for each vote; ProposalId => Vec s.t. sum(VoteId::Outcomes) => ProposalId::Outcome type VoteId: Parameter + Member @@ -37,174 +32,88 @@ pub trait VoteYesNo: System { + MaybeSerializeDeserialize + Debug + Zero; +} + +// ~~ Values ~~ - /// An instance of the shares module - type ShareData: GroupMembership - + RegisterShareGroup - + ReservableProfile - + LockableProfile - + ShareBank; +#[derive(Clone, Debug, Eq, PartialEq, Encode)] +pub struct VoteIdCounterStore { + pub nonce: T::VoteId, } -/// The share identifier type -pub type Shares = - <::ShareData as ShareRegistration<::AccountId>>::ShareId; - -/// The organization identifier type -pub type OrgId = - <::ShareData as ShareRegistration<::AccountId>>::OrgId; - -const MODULE: &str = "VoteYesNo"; -const CREATE_SHARE_WEIGHTED_PERCENTAGE_VOTE_NO_EXPIRY: &str = - "create_share_weighted_percentage_threshold_vote"; -const CREATE_SHARE_WEIGHTED_COUNT_VOTE_NO_EXPIRY: &str = - "create_share_weighted_count_threshold_vote"; -const CREATE_1P1V_PERCENTAGE_VOTE_NO_EXPIRY: &str = "create_1p1v_percentage_threshold_vote"; -const CREATE_1P1V_COUNT_VOTE_NO_EXPIRY: &str = "create_1p1v_count_threshold_vote"; -const SUBMIT_VOTE: &str = "submit_vote"; - -/// Arguments for creating a share weighted vote with thresholds based on percents -#[derive(codec::Encode)] -pub struct CreateShareWeightedPercentageVoteArgs { - organization: OrgId, - share_id: ShareId, - support_requirement: Permill, - turnout_requirement: Permill, +#[derive(Clone, Debug, Eq, PartialEq, Encode)] +pub struct OpenVoteCounterStore { + pub counter: u32, } -/// Arguments for creating a share weighted vote with thresholds based on signal amounts -#[derive(codec::Encode)] -pub struct CreateShareWeightedCountVoteArgs { - organization: OrgId, - share_id: ShareId, - support_requirement: T::Signal, - turnout_requirement: T::Signal, +// ~~ Maps ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct VoteStateStore { + #[store(returns = VoteState::BlockNumber, ::IpfsReference>)] + pub vote: T::VoteId, } -/// Arguments for creating a 1p1v vote with thresholds based on signal amounts -#[derive(codec::Encode)] -pub struct Create1P1VPercentageVoteArgs { - organization: OrgId, - share_id: ShareId, - support_requirement: T::Signal, - turnout_requirement: T::Signal, +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct TotalSignalIssuanceStore { + #[store(returns = T::Signal)] + pub vote: T::VoteId, } -/// Arguments for creating a 1p1v vote with thresholds based on signal amounts -#[derive(codec::Encode)] -pub struct Create1P1VCountVoteArgs { - organization: OrgId, - share_id: ShareId, - support_requirement: T::Signal, - turnout_requirement: T::Signal, +#[derive(Clone, Debug, Eq, PartialEq, Store, Encode)] +pub struct VoteLoggerStore { + #[store(returns = VoteVector::IpfsReference>)] + pub vote: T::VoteId, + pub who: ::AccountId, } -/// Arguments for submitting a vote -#[derive(codec::Encode)] -pub struct SubmitVoteArgs { - organization: OrgId, - share_id: ShareId, - vote_id: T::Signal, - voter: ::AccountId, - direction: VoterYesNoView, - magnitude: Option, +// ~~ Calls ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct CreateWeightedPercentageThresholdVoteCall { + pub topic: Option<::IpfsReference>, + pub organization: T::OrgId, + pub passage_threshold_pct: Permill, + pub turnout_threshold_pct: Permill, + pub duration: Option<::BlockNumber>, } -/// Create share weighted percentage threshold vote in the context of an organizational share group -pub fn create_share_weighted_percentage_vote( - organization: OrgId, - share_id: ShareId, - support_requirement: Permill, - turnout_requirement: Permill, -) -> Call> { - Call::new( - MODULE, - CREATE_SHARE_WEIGHTED_PERCENTAGE_VOTE_NO_EXPIRY, - CreateShareWeightedPercentageVoteArgs { - organization, - share_id, - support_requirement, - turnout_requirement, - }, - ) +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct CreateWeightedCountThresholdVoteCall { + pub topic: Option<::IpfsReference>, + pub organization: T::OrgId, + pub support_requirement: T::Signal, + pub turnout_requirement: T::Signal, + pub duration: Option<::BlockNumber>, } -/// Create share weighted count threshold vote in the context of an organizational share group -pub fn create_share_weighted_count_vote( - organization: OrgId, - share_id: ShareId, - support_requirement: T::Signal, - turnout_requirement: T::Signal, -) -> Call> { - Call::new( - MODULE, - CREATE_SHARE_WEIGHTED_COUNT_VOTE_NO_EXPIRY, - CreateShareWeightedCountVoteArgs { - organization, - share_id, - support_requirement, - turnout_requirement, - }, - ) +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct CreateUnanimousConsentVoteCall { + pub topic: Option<::IpfsReference>, + pub organization: T::OrgId, + pub duration: Option<::BlockNumber>, } -/// Create share weighted count threshold vote in the context of an organizational share group -pub fn create_1p1v_percentage_vote( - organization: OrgId, - share_id: ShareId, - support_requirement: T::Signal, - turnout_requirement: T::Signal, -) -> Call> { - Call::new( - MODULE, - CREATE_1P1V_PERCENTAGE_VOTE_NO_EXPIRY, - Create1P1VPercentageVoteArgs { - organization, - share_id, - support_requirement, - turnout_requirement, - }, - ) +#[derive(Clone, Debug, Eq, PartialEq, Call, Encode)] +pub struct SubmitVoteCall { + pub vote_id: T::VoteId, + pub direction: VoterView, + pub magnitude: Option, + pub justification: Option<::IpfsReference>, } -/// Create 1 account 1 vote count threshold vote in the context of an organizational share group -pub fn create_1p1v_count_vote( - organization: OrgId, - share_id: ShareId, - support_requirement: T::Signal, - turnout_requirement: T::Signal, -) -> Call> { - Call::new( - MODULE, - CREATE_1P1V_COUNT_VOTE_NO_EXPIRY, - Create1P1VCountVoteArgs { - organization, - share_id, - support_requirement, - turnout_requirement, - }, - ) +// ~~ Events ~~ + +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct NewVoteStartedEvent { + pub caller: ::AccountId, + pub org: T::OrgId, + pub new_vote_id: T::VoteId, } -/// Submits a vote -pub fn submit_vote( - organization: OrgId, - share_id: ShareId, - vote_id: T::Signal, - voter: ::AccountId, - direction: VoterYesNoView, - magnitude: Option, -) -> Call> { - Call::new( - MODULE, - SUBMIT_VOTE, - SubmitVoteArgs { - organization, - share_id, - vote_id, - voter, - direction, - magnitude, - }, - ) +#[derive(Clone, Debug, Eq, PartialEq, Event, Decode)] +pub struct VotedEvent { + pub vote_id: T::VoteId, + pub voter: ::AccountId, + pub view: VoterView, } diff --git a/client/src/sunshine.rs b/client/src/sunshine.rs index ed903c6..89e6e4a 100644 --- a/client/src/sunshine.rs +++ b/client/src/sunshine.rs @@ -1,53 +1,209 @@ -use crate::error::Error; +use crate::error::{Error, Result}; #[cfg(feature = "light-client")] use crate::light_client::ChainType; -use crate::runtime::{Client, Pair, Runtime}; //XtBuilder - // use crate::srml::org::*; -use ipfs_embed::{Config, Store}; +use crate::runtime::{Client, Pair, PairSigner, Runtime}; +use crate::srml::org::*; +use ipfs_embed::Store; use ipld_block_builder::{BlockBuilder, Codec}; -use sp_core::Pair as _; -use std::path::Path; -use substrate_subxt::system::*; +use keystore::{DeviceKey, KeyStore, Password}; +// use std::path::Path; +use substrate_subxt::sp_core::crypto::Pair as SubPair; +use substrate_subxt::sp_runtime::AccountId32; //Signer +use utils_identity::cid::CidBytes; -pub struct Sunshine { - account_id: ::AccountId, - subxt: Client, - // xt: XtBuilder, - ipld: BlockBuilder, +#[derive(new)] +pub struct SunClient { + client: Client, + keystore: KeyStore, + pub ipld: BlockBuilder, } -impl Sunshine { - //#[cfg(not(feature = "light-client"))] - pub async fn new>(path: T, signer: Pair) -> Result { - let db = sled::open(path)?; - let ipld_tree = db.open_tree("ipld_tree")?; - let config = Config::from_tree(ipld_tree); - let store = Store::new(config)?; - let codec = Codec::new(); - let ipld = BlockBuilder::new(store, codec); - let account_id = signer.public().into(); - let subxt = crate::runtime::ClientBuilder::new().build().await?; - // let xt = subxt.xt(signer, None).await?; - Ok(Self { - account_id, - subxt, - // xt, - ipld, - }) +impl SunClient { + /// Set device key, directly from substrate-identity to use with keystore + pub fn has_device_key(&self) -> bool { + self.keystore.is_initialized() + } + /// Set device key, directly from substrate-identity to use with keystore + pub fn set_device_key( + &self, + dk: &DeviceKey, + password: &Password, + force: bool, + ) -> Result { + if self.keystore.is_initialized() && !force { + return Err(Error::KeystoreInitialized); + } + let pair = Pair::from_seed(&::Seed::from(*dk.expose_secret())); + self.keystore.initialize(&dk, &password)?; + Ok(pair.public().into()) + } + /// Returns a signer for alice + pub fn signer(&self) -> Result { + // fetch device key from disk every time to make sure account is unlocked. + let dk = self.keystore.device_key()?; + Ok(PairSigner::new(Pair::from_seed( + &::Seed::from(*dk.expose_secret()), + ))) + } + /// Register flat organization + pub async fn register_flat_org( + &self, + sudo: Option, + parent_org: Option, + constitution: CidBytes, + members: &[AccountId32], + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .register_flat_org_and_watch(&signer, sudo, parent_org, constitution, members) + .await? + .new_flat_organization_registered() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Register weighted organization + pub async fn register_weighted_org( + &self, + sudo: Option, + parent_org: Option, + constitution: CidBytes, + weighted_members: &[(AccountId32, u64)], + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .register_weighted_org_and_watch( + &signer, + sudo, + parent_org, + constitution, + weighted_members, + ) + .await? + .new_weighted_organization_registered() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Issue shares + pub async fn issue_shares( + &self, + organization: u64, + who: AccountId32, + shares: u64, + ) -> Result<()> { + let signer = self.signer()?; + self.client + .issue_shares_and_watch(&signer, organization, &who, shares) + .await? + .shares_issued() + .map_err(|e| substrate_subxt::Error::Codec(e))?; + Ok(()) + } + /// Burn shares + pub async fn burn_shares( + &self, + organization: u64, + who: AccountId32, + shares: u64, + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .burn_shares_and_watch(&signer, organization, &who, shares) + .await? + .shares_burned() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Batch issue shares + pub async fn batch_issue_shares( + &self, + organization: u64, + new_accounts: &[(AccountId32, u64)], + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .batch_issue_shares_and_watch(&signer, organization, new_accounts) + .await? + .shares_batch_issued() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Batch burn shares + pub async fn batch_burn_shares( + &self, + organization: u64, + old_accounts: &[(AccountId32, u64)], + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .batch_burn_shares_and_watch(&signer, organization, old_accounts) + .await? + .shares_batch_burned() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Reserves shares for alice + pub async fn reserve_shares( + &self, + org: u64, + who: &AccountId32, + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .reserve_shares_and_watch(&signer, org, who) + .await? + .shares_reserved() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Reserves shares for alice + pub async fn unreserve_shares( + &self, + org: u64, + who: &AccountId32, + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .unreserve_shares_and_watch(&signer, org, who) + .await? + .shares_un_reserved() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Lock shares for alice + pub async fn lock_shares( + &self, + org: u64, + who: &AccountId32, + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .lock_shares_and_watch(&signer, org, who) + .await? + .shares_locked() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) + } + /// Unlock shares for alice + pub async fn unlock_shares( + &self, + org: u64, + who: &AccountId32, + ) -> Result> { + let signer = self.signer()?; + self.client + .clone() + .unlock_shares_and_watch(&signer, org, who) + .await? + .shares_unlocked() + .map_err(|e| substrate_subxt::Error::Codec(e))? + .ok_or(Error::EventNotFound) } - - // pub async fn reserve_shares( - // &self, - // org: u32, - // share: u32, - // ) -> Result, Error> { - // self.xt - // .clone() - // .watch() - // .reserve_shares(org, share, &self.account_id) - // .await? - // .shares_reserved() - // .map_err(|e| substrate_subxt::Error::Codec(e))? - // .ok_or(Error::EventNotFound) - // } } diff --git a/modules/bank/src/lib.rs b/modules/bank/src/lib.rs index 79e8abe..d42475e 100644 --- a/modules/bank/src/lib.rs +++ b/modules/bank/src/lib.rs @@ -11,17 +11,16 @@ mod tests; use util::{ bank::{ - BankMapID, BankState, DepositInfo, InternalTransferInfo, OnChainTreasuryID, - ReservationInfo, WithdrawalPermissions, + BankMapID, BankState, DepositInfo, InternalTransferInfo, OnChainTreasuryID, ReservationInfo, }, traits::{ CalculateOwnership, CheckBankBalances, CommitAndTransfer, CommitSpendReservation, DefaultBankPermissions, DepositIntoBank, DepositSpendOps, DepositsAndSpends, ExecuteSpends, - FreeToReserved, GenerateUniqueID, GetGroupSize, GroupMembership, IDIsAvailable, Increment, + FreeToReserved, GenerateUniqueID, GroupMembership, IDIsAvailable, Increment, MoveFundsOutCommittedOnly, MoveFundsOutUnCommittedOnly, OnChainBank, OrganizationSupervisorPermissions, RegisterAccount, ReservationMachine, SeededGenerateUniqueID, ShareInformation, ShareIssuance, TermSheetExit, - }, // if sudo, import ChainSudoPermissions + }, // ChainSudoPermissions, GetGroupSize }; use codec::Codec; @@ -75,13 +74,13 @@ decl_event!( ::BankAssociatedId, Balance = BalanceOf, { - RegisteredNewOnChainBank(AccountId, OnChainTreasuryID, Balance, OrgId, WithdrawalPermissions), + RegisteredNewOnChainBank(AccountId, OnChainTreasuryID, Balance, OrgId, Option), CapitalDepositedIntoOnChainBankAccount(AccountId, OnChainTreasuryID, Balance, IpfsReference), - SpendReservedForBankAccount(AccountId, OnChainTreasuryID, BankAssociatedId, IpfsReference, Balance, WithdrawalPermissions), + SpendReservedForBankAccount(AccountId, OnChainTreasuryID, BankAssociatedId, IpfsReference, Balance, OrgId), CommitSpendBeforeInternalTransfer(AccountId, OnChainTreasuryID, BankAssociatedId, Balance), UnReserveUncommittedReservationToMakeFree(AccountId, OnChainTreasuryID, BankAssociatedId, Balance), UnReserveCommittedReservationToMakeFree(AccountId, OnChainTreasuryID, BankAssociatedId, Balance), - InternalTransferExecutedAndSpendingPowerDoledOutToController(AccountId, OnChainTreasuryID, IpfsReference, BankAssociatedId, Balance, WithdrawalPermissions), + InternalTransferExecutedAndSpendingPowerDoledOutToController(AccountId, OnChainTreasuryID, IpfsReference, BankAssociatedId, Balance, OrgId), SpendRequestForInternalTransferApprovedAndExecuted(OnChainTreasuryID, AccountId, Balance, BankAssociatedId), AccountLeftMembershipAndWithdrewProportionOfFreeCapitalInBank(OnChainTreasuryID, AccountId, Balance), } @@ -105,8 +104,7 @@ decl_error! { NotEnoughFundsInSpendReservationUnCommittedToSatisfyUnreserveUnCommittedRequest, NotEnoughFundsInBankReservedToSatisfyUnReserveUnComittedRequest, RegistrationMustDepositAboveModuleMinimum, - AccountMatchesNoneOfTwoControllers, - AccountHasNoWeightedOwnershipInOrg, + AccountHasNoOwnershipInOrg, BankAccountNotFoundForCommittingAndTransferringInOneStep, BankAccountNotFoundForUnReservingCommitted, BankAccountNotFoundForUnReservingUnCommitted, @@ -134,7 +132,7 @@ decl_storage! { /// WARNING: do not append a prefix because the keyspace is used directly for checking uniqueness of `OnChainTreasuryId` /// TODO: pre-reserve any known ModuleId's that could be accidentally generated that already exist elsewhere pub BankStores get(fn bank_stores): map - hasher(blake2_128_concat) OnChainTreasuryID => Option, BalanceOf>>; + hasher(blake2_128_concat) OnChainTreasuryID => Option>>; /// All deposits made into the joint bank account represented by OnChainTreasuryID pub Deposits get(fn deposits): double_map @@ -144,12 +142,12 @@ decl_storage! { /// Spend reservations which designated a committee for formally transferring ownership to specific destination addresses pub SpendReservations get(fn spend_reservations): double_map hasher(blake2_128_concat) OnChainTreasuryID, - hasher(blake2_128_concat) T::BankAssociatedId => Option, WithdrawalPermissions>>; + hasher(blake2_128_concat) T::BankAssociatedId => Option, T::OrgId>>; /// Internal transfers of control over capital that allow transfer liquidity rights to the current controller pub InternalTransfers get(fn internal_transfers): double_map hasher(blake2_128_concat) OnChainTreasuryID, - hasher(blake2_128_concat) T::BankAssociatedId => Option, WithdrawalPermissions>>; + hasher(blake2_128_concat) T::BankAssociatedId => Option, T::OrgId>>; } } @@ -177,13 +175,14 @@ decl_module! { origin, seed: BalanceOf, hosting_org: T::OrgId, // pre-requisite is registered organization - bank_controller: WithdrawalPermissions, + bank_operator: Option, ) -> DispatchResult { let seeder = ensure_signed(origin)?; - let authentication = Self::can_register_account(seeder.clone(), hosting_org) && Self::withdrawal_permissions_satisfy_org_standards(hosting_org, bank_controller.clone()); + let authentication = Self::can_register_account(seeder.clone(), hosting_org) && + if let Some(op) = bank_operator { Self::operator_satisfies_requirements(hosting_org, op) } else {true}; ensure!(authentication, Error::::CannotRegisterBankAccountBCPermissionsCheckFails); - let new_bank_id = Self::register_account(hosting_org, seeder.clone(), seed, bank_controller.clone())?; - Self::deposit_event(RawEvent::RegisteredNewOnChainBank(seeder, new_bank_id, seed, hosting_org, bank_controller)); + let new_bank_id = Self::register_account(hosting_org, seeder.clone(), seed, bank_operator)?; + Self::deposit_event(RawEvent::RegisteredNewOnChainBank(seeder, new_bank_id, seed, hosting_org, bank_operator)); Ok(()) } #[weight = 0] @@ -192,12 +191,12 @@ decl_module! { bank_id: OnChainTreasuryID, reason: T::IpfsReference, amount: BalanceOf, - controller: WithdrawalPermissions, + controller: T::OrgId, ) -> DispatchResult { let reserver = ensure_signed(origin)?; let authentication = Self::can_reserve_for_spend(reserver.clone(), bank_id)?; ensure!(authentication, Error::::CannotReserveSpendBCPermissionsCheckFails); - let new_reservation_id = Self::reserve_for_spend(bank_id, reason.clone(), amount, controller.clone())?; + let new_reservation_id = Self::reserve_for_spend(bank_id, reason.clone(), amount, controller)?; Self::deposit_event(RawEvent::SpendReservedForBankAccount(reserver, bank_id, new_reservation_id, reason, amount, controller)); Ok(()) } @@ -208,7 +207,6 @@ decl_module! { reservation_id: T::BankAssociatedId, reason: T::IpfsReference, amount: BalanceOf, - future_controller: WithdrawalPermissions, ) -> DispatchResult { let committer = ensure_signed(origin)?; let authentication = Self::can_commit_reserved_spend_for_transfer(committer.clone(), bank_id)?; @@ -252,12 +250,12 @@ decl_module! { reason: T::IpfsReference, reservation_id: T::BankAssociatedId, amount: BalanceOf, - committed_controller: WithdrawalPermissions, + committed_controller: T::OrgId, ) -> DispatchResult { let qualified_spend_reservation_controller = ensure_signed(origin)?; let authentication = Self::can_transfer_spending_power(qualified_spend_reservation_controller.clone(), bank_id)?; ensure!(authentication, Error::::CannotTransferSpendingPowerBCPermissionsCheckFails); - Self::transfer_spending_power(bank_id, reason.clone(), reservation_id, amount, committed_controller.clone())?; + Self::transfer_spending_power(bank_id, reason.clone(), reservation_id, amount, committed_controller)?; Self::deposit_event(RawEvent::InternalTransferExecutedAndSpendingPowerDoledOutToController(qualified_spend_reservation_controller, bank_id, reason, reservation_id, amount, committed_controller)); Ok(()) } @@ -331,37 +329,23 @@ impl Module { None } } - pub fn get_reservations_for_governance_config( + pub fn get_reservations_for_organization( bank_id: OnChainTreasuryID, - invoker: WithdrawalPermissions, - ) -> Option< - Vec< - ReservationInfo< - T::IpfsReference, - BalanceOf, - WithdrawalPermissions, - >, - >, - > { + invoker: T::OrgId, + ) -> Option, T::OrgId>>> { let ret = >::iter() .filter(|(id, _, reservation)| id == &bank_id && reservation.controller() == invoker) .map(|(_, _, reservation)| reservation) - .collect::, - WithdrawalPermissions, - >, - >>(); + .collect::, T::OrgId>>>(); if ret.is_empty() { None } else { Some(ret) } } - pub fn total_capital_reserved_for_governance_config( + pub fn total_capital_reserved_for_organization( bank_id: OnChainTreasuryID, - invoker: WithdrawalPermissions, + invoker: T::OrgId, ) -> BalanceOf { >::iter() .filter(|(id, _, reservation)| id == &bank_id && reservation.controller() == invoker) @@ -382,27 +366,15 @@ impl Module { } pub fn get_transfers_for_governance_config( bank_id: OnChainTreasuryID, - invoker: WithdrawalPermissions, + invoker: T::OrgId, ) -> Option< - Vec< - InternalTransferInfo< - T::BankAssociatedId, - T::IpfsReference, - BalanceOf, - WithdrawalPermissions, - >, - >, + Vec, T::OrgId>>, > { let ret = >::iter() .filter(|(id, _, transfer)| id == &bank_id && transfer.controller() == invoker) .map(|(_, _, transfer)| transfer) .collect::, - WithdrawalPermissions, - >, + InternalTransferInfo, T::OrgId>, >>(); if ret.is_empty() { None @@ -412,7 +384,7 @@ impl Module { } pub fn total_capital_transferred_to_governance_config( bank_id: OnChainTreasuryID, - invoker: WithdrawalPermissions, + invoker: T::OrgId, ) -> BalanceOf { >::iter() .filter(|(id, _, transfer)| id == &bank_id && transfer.controller() == invoker) @@ -467,19 +439,12 @@ impl OnChainBank for Module { type AssociatedId = T::BankAssociatedId; } -impl - RegisterAccount< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - > for Module -{ +impl RegisterAccount> for Module { fn register_account( owners: T::OrgId, from: T::AccountId, amount: BalanceOf, - operators: WithdrawalPermissions, + operators: Option, ) -> Result { ensure!( amount >= T::MinimumInitialDeposit::get(), @@ -505,50 +470,23 @@ impl } } -impl - CalculateOwnership< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - Permill, - > for Module -{ +impl CalculateOwnership, Permill> for Module { fn calculate_proportion_ownership_for_account( account: T::AccountId, - group: WithdrawalPermissions, + group: T::OrgId, ) -> Result { - match group { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => { - // assumes that we never use this with acc1 == acc2; use sudo in that situation - if acc1 == account || acc2 == account { - Ok(Permill::from_percent(50)) - } else { - Err(Error::::AccountMatchesNoneOfTwoControllers.into()) - } - } - WithdrawalPermissions::JointOrgAccount(org_id) => { - let issuance = >::total_issuance(org_id); - if issuance > T::Shares::zero() { - // weighted group - let acc_ownership = >::get_share_profile(org_id, &account) - .ok_or(Error::::AccountHasNoWeightedOwnershipInOrg)?; - Ok(Permill::from_rational_approximation( - acc_ownership.total(), - issuance, - )) - } else { - // flat group - let organization_size = >::get_size_of_group(org_id); - Ok(Permill::from_rational_approximation(1, organization_size)) - } - } - } + let issuance = >::total_issuance(group); + let acc_ownership = >::get_share_profile(group, &account) + .ok_or(Error::::AccountHasNoOwnershipInOrg)?; + Ok(Permill::from_rational_approximation( + acc_ownership.total(), + issuance, + )) } fn calculate_proportional_amount_for_account( amount: BalanceOf, account: T::AccountId, - group: WithdrawalPermissions, + group: T::OrgId, ) -> Result, DispatchError> { let proportion_due = Self::calculate_proportion_ownership_for_account(account, group)?; Ok(proportion_due * amount) @@ -556,7 +494,7 @@ impl } impl DepositsAndSpends> for Module { - type Bank = BankState, BalanceOf>; + type Bank = BankState<::OrgId, BalanceOf>; fn make_infallible_deposit_into_free(bank: Self::Bank, amount: BalanceOf) -> Self::Bank { bank.deposit_into_free(amount) } @@ -599,25 +537,13 @@ impl CheckBankBalances> for Module { // We could NOT have the extra storage lookup in here but // the API design is much more extensible this way. See issue #85 -impl - DefaultBankPermissions< - T::OrgId, - T::AccountId, - BalanceOf, - WithdrawalPermissions, - > for Module -{ +impl DefaultBankPermissions> for Module { fn can_register_account(account: T::AccountId, on_behalf_of: T::OrgId) -> bool { // only the organization supervisor can register a bank account >::is_organization_supervisor(on_behalf_of, &account) } - fn withdrawal_permissions_satisfy_org_standards( - _org: T::OrgId, - _withdrawal_permissions: WithdrawalPermissions, - ) -> bool { - // an example might require that withdrawal_permissions is a subgroup - //or contains members of OrgId but I don't think that's necessary to - //impl as a default...sometimes no default is OK + fn operator_satisfies_requirements(_org: T::OrgId, _withdrawal_permissions: T::OrgId) -> bool { + // TODO: add default as a subgroup check? does this relation imply revocability of control true } // bank.operators() can make spend reservations (indicates funding intent by beginning the formal shift of capital control) @@ -627,11 +553,11 @@ impl ) -> Result { let bank_account = >::get(bank).ok_or(Error::::BankAccountNotFoundForSpendReservation)?; - let ret_bool = match bank_account.operators() { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => acc1 == account || acc2 == account, - WithdrawalPermissions::JointOrgAccount(org_id) => { - >::is_member_of_group(org_id, &account) - } + let ret_bool = if let Some(org_id) = bank_account.operators() { + >::is_member_of_group(org_id, &account) + } else { + // if bank.operators().is_none() + >::is_member_of_group(bank_account.owners(), &account) }; Ok(ret_bool) } @@ -642,11 +568,11 @@ impl ) -> Result { let bank_account = >::get(bank).ok_or(Error::::BankAccountNotFoundForSpendCommitment)?; - let ret_bool = match bank_account.operators() { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => acc1 == account || acc2 == account, - WithdrawalPermissions::JointOrgAccount(org_id) => { - >::is_member_of_group(org_id, &account) - } + let ret_bool = if let Some(org_id) = bank_account.operators() { + >::is_member_of_group(org_id, &account) + } else { + // if bank.operators().is_none() + >::is_member_of_group(bank_account.owners(), &account) }; Ok(ret_bool) } @@ -657,15 +583,12 @@ impl ) -> Result { let bank_account = >::get(bank) .ok_or(Error::::BankAccountNotFoundForUnReservingUnCommitted)?; - let ret_bool = >::is_member_of_group(bank_account.owners(), &account) - || match bank_account.operators() { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => { - acc1 == account || acc2 == account - } - WithdrawalPermissions::JointOrgAccount(org_id) => { - >::is_member_of_group(org_id, &account) - } - }; + let ret_bool = if let Some(org_id) = bank_account.operators() { + >::is_member_of_group(org_id, &account) + } else { + // if bank.operators().is_none() + >::is_member_of_group(bank_account.owners(), &account) + }; Ok(ret_bool) } // ONLY bank.operators() can unreserve committed funds @@ -675,11 +598,11 @@ impl ) -> Result { let bank_account = >::get(bank) .ok_or(Error::::BankAccountNotFoundForUnReservingCommitted)?; - let ret_bool = match bank_account.operators() { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => acc1 == account || acc2 == account, - WithdrawalPermissions::JointOrgAccount(org_id) => { - >::is_member_of_group(org_id, &account) - } + let ret_bool = if let Some(org_id) = bank_account.operators() { + >::is_member_of_group(org_id, &account) + } else { + // if bank.operators().is_none() + >::is_member_of_group(bank_account.owners(), &account) }; Ok(ret_bool) } @@ -690,15 +613,12 @@ impl ) -> Result { let bank_account = >::get(bank) .ok_or(Error::::BankAccountNotFoundForUnReservingUnCommitted)?; - let ret_bool = >::is_member_of_group(bank_account.owners(), &account) - || match bank_account.operators() { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => { - acc1 == account || acc2 == account - } - WithdrawalPermissions::JointOrgAccount(org_id) => { - >::is_member_of_group(org_id, &account) - } - }; + let ret_bool = if let Some(org_id) = bank_account.operators() { + >::is_member_of_group(org_id, &account) + } else { + // if bank.operators().is_none() + >::is_member_of_group(bank_account.owners(), &account) + }; Ok(ret_bool) } // supervisor-oriented permissions for committing and transferring withdrawal permissions in a single step @@ -708,28 +628,18 @@ impl ) -> Result { let bank_account = >::get(bank) .ok_or(Error::::BankAccountNotFoundForCommittingAndTransferringInOneStep)?; - Ok( + let ret_bool = if let Some(org_id) = bank_account.operators() { + >::is_organization_supervisor(org_id, &account) + } else { + // if bank.operators().is_none() >::is_organization_supervisor(bank_account.owners(), &account) - || match bank_account.operators() { - WithdrawalPermissions::TwoAccounts(acc1, acc2) => { - acc1 == account || acc2 == account - } - WithdrawalPermissions::JointOrgAccount(org_id) => { - >::is_organization_supervisor(org_id, &account) - } - }, - ) + }; + Ok(ret_bool) } } -impl - DepositIntoBank< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - > for Module +impl DepositIntoBank, T::IpfsReference> + for Module { fn deposit_into_bank( from: T::AccountId, @@ -756,20 +666,14 @@ impl } } -impl - ReservationMachine< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - > for Module +impl ReservationMachine, T::IpfsReference> + for Module { fn reserve_for_spend( bank_id: Self::TreasuryId, reason: T::IpfsReference, amount: BalanceOf, - controller: WithdrawalPermissions, + controller: T::OrgId, ) -> Result { let bank_account = >::get(bank_id) .ok_or(Error::::BankAccountNotFoundForSpendReservation)?; @@ -861,7 +765,7 @@ impl reservation_id: Self::AssociatedId, amount: BalanceOf, // move control of funds to new outer group which can reserve or withdraw directly - new_controller: WithdrawalPermissions, + new_controller: T::OrgId, ) -> Result { let _ = >::get(bank_id) .ok_or(Error::::BankAccountNotFoundForInternalTransfer)?; @@ -885,21 +789,15 @@ impl } } -impl - CommitAndTransfer< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - > for Module +impl CommitAndTransfer, T::IpfsReference> + for Module { fn commit_and_transfer_spending_power( bank_id: Self::TreasuryId, reservation_id: Self::AssociatedId, reason: T::IpfsReference, amount: BalanceOf, - new_controller: WithdrawalPermissions, + new_controller: T::OrgId, ) -> Result { let _ = >::get(bank_id) .ok_or(Error::::BankAccountNotFoundForInternalTransfer)?; @@ -923,15 +821,7 @@ impl } } -impl - ExecuteSpends< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - > for Module -{ +impl ExecuteSpends, T::IpfsReference> for Module { /// This method authenticates the spend by checking that the caller /// input follows the same shape as the bank's controller... /// => any method that calls this one will need to define local @@ -1003,7 +893,7 @@ impl TermSheetExit> for Module { let withdrawal_amount = Self::calculate_proportional_amount_for_account( bank_account.free(), rage_quitter.clone(), - WithdrawalPermissions::JointOrgAccount(bank_account.owners()), + bank_account.owners(), )?; // // burn the shares first diff --git a/modules/bounty/src/lib.rs b/modules/bounty/src/lib.rs index c985fb5..ae1a24e 100644 --- a/modules/bounty/src/lib.rs +++ b/modules/bounty/src/lib.rs @@ -7,6 +7,13 @@ #![cfg_attr(not(feature = "std"), no_std)] //! The bounty module allows registered organizations with on-chain bank accounts to //! register as a foundation to post bounties and supervise ongoing grant pursuits. +//! +//! # (Id, Id) Design Justification +//! "WHY so many double_maps in storage with (BountyId, BountyId)?" +//! -> the prefix is the identifier for the `BountyId` for the bounty in question and the suffix +//! `BountyId` is the unique identifier for this particular object associated with the bounty +//! ...we use this structure for efficient clean up via double_map.remove_prefix() once +//! a bounty needs to be removed from the storage state so that we can efficiently remove all associated state mod tests; @@ -26,37 +33,33 @@ use sp_runtime::{ use sp_std::{fmt::Debug, prelude::*}; use util::{ - bank::{BankMapID, OnChainTreasuryID, WithdrawalPermissions}, + bank::OnChainTreasuryID, bounty::{ ApplicationState, BountyInformation, BountyMapID, GrantApplication, MilestoneStatus, MilestoneSubmission, ReviewBoard, TeamID, }, //BountyPaymentTracker organization::TermsOfAgreement, traits::{ - ApproveGrant, ApproveWithoutTransfer, CalculateOwnership, CheckBankBalances, - CommitAndTransfer, CreateBounty, DepositIntoBank, DepositsAndSpends, ExecuteSpends, - GenerateUniqueID, GetTeamOrg, GetVoteOutcome, IDIsAvailable, OnChainBank, OpenVote, + ApproveGrant, ApproveWithoutTransfer, BountyPermissions, CommitAndTransfer, CreateBounty, + GenerateUniqueID, GetTeamOrg, GetVoteOutcome, GroupMembership, IDIsAvailable, OpenVote, RegisterAccount, RegisterFoundation, RegisterOrganization, ReservationMachine, SeededGenerateUniqueID, SetMakeTransfer, SpendApprovedGrant, StartReview, StartTeamConsentPetition, SubmitGrantApplication, SubmitMilestone, - SuperviseGrantApplication, TermSheetExit, UseTermsOfAgreement, + SuperviseGrantApplication, UseTermsOfAgreement, }, // UpdateVoteTopic, VoteOnProposal vote::{ThresholdConfig, VoteOutcome}, }; /// The balances type for this module pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; + <::Currency as Currency<::AccountId>>::Balance; /// The associate identifier for the bank module -pub type BankAssociatedId = <::Bank as OnChainBank>::AssociatedId; +pub type BankAssociatedId = ::BankAssociatedId; -pub trait Trait: frame_system::Trait + org::Trait + vote::Trait { +pub trait Trait: frame_system::Trait + org::Trait + vote::Trait + bank::Trait { /// The overarching event type type Event: From> + Into<::Event>; - /// The currency type for on-chain transactions - type Currency: Currency; - /// The bounty identifier in this module type BountyId: Parameter + Member @@ -70,51 +73,6 @@ pub trait Trait: frame_system::Trait + org::Trait + vote::Trait { + PartialEq + Zero; - /// The bank module - type Bank: IDIsAvailable - + IDIsAvailable<(OnChainTreasuryID, BankMapID, BankAssociatedId)> - + GenerateUniqueID - + SeededGenerateUniqueID, (OnChainTreasuryID, BankMapID)> - + OnChainBank - + RegisterAccount< - Self::OrgId, - Self::AccountId, - WithdrawalPermissions, - BalanceOf, - > + CalculateOwnership< - Self::OrgId, - Self::AccountId, - WithdrawalPermissions, - BalanceOf, - Permill, - > + DepositsAndSpends> - + CheckBankBalances> - + DepositIntoBank< - Self::OrgId, - Self::AccountId, - WithdrawalPermissions, - BalanceOf, - Self::IpfsReference, - > + ReservationMachine< - Self::OrgId, - Self::AccountId, - WithdrawalPermissions, - BalanceOf, - Self::IpfsReference, - > + ExecuteSpends< - Self::OrgId, - Self::AccountId, - WithdrawalPermissions, - BalanceOf, - Self::IpfsReference, - > + CommitAndTransfer< - Self::OrgId, - Self::AccountId, - WithdrawalPermissions, - BalanceOf, - Self::IpfsReference, - > + TermSheetExit>; - /// This is the minimum percent of the total bounty that must be reserved as collateral type MinimumBountyCollateralRatio: Get; @@ -136,15 +94,15 @@ decl_event!( { FoundationRegisteredFromOnChainBank(OrgId, OnChainTreasuryID), FoundationPostedBounty(AccountId, OrgId, BountyId, OnChainTreasuryID, IpfsReference, Currency, Currency), - // BountyId, Application Id (u32s) + // BountyId, Application Id GrantApplicationSubmittedForBounty(AccountId, BountyId, BountyId, IpfsReference, Currency), - // BountyId, Application Id (u32s) + // BountyId, Application Id ApplicationReviewTriggered(AccountId, BountyId, BountyId, ApplicationState, VoteId>), SudoApprovedApplication(AccountId, BountyId, BountyId, ApplicationState, VoteId>), ApplicationPolled(BountyId, BountyId, ApplicationState, VoteId>), - // BountyId, ApplicationId, MilestoneId (u32s) + // BountyId, ApplicationId, MilestoneId MilestoneSubmitted(AccountId, BountyId, BountyId, BountyId), - // BountyId, MilestoneId (u32s) + // BountyId, MilestoneId MilestoneReviewTriggered(AccountId, BountyId, BountyId, MilestoneStatus), SudoApprovedMilestone(AccountId, BountyId, BountyId, MilestoneStatus), MilestonePolled(AccountId, BountyId, BountyId, MilestoneStatus), @@ -169,7 +127,9 @@ decl_error! { CannotSudoApproveIfBountyDNE, CannotSudoApproveAppIfNotAssignedSudo, CannotSudoApproveIfGrantAppDNE, + CannotSubmitMilestoneIfBountyDNE, CannotSubmitMilestoneIfApplicationDNE, + CannotSubmitMilestoneIfApplicationNotApprovedAndLive, CannotTriggerMilestoneReviewIfBountyDNE, CannotTriggerMilestoneReviewUnlessMember, CannotSudoApproveMilestoneIfNotAssignedSudo, @@ -185,7 +145,13 @@ decl_error! { ApplicationMustBeSubmittedAwaitingResponseToTriggerReview, ApplicationMustApprovedAndLiveWithTeamIDMatchingInput, MilestoneSubmissionRequestExceedsApprovedApplicationsLimit, + AccountNotAuthorizedToCreateBounty, + NotAuthorizedToSubmitGrantApplication, AccountNotAuthorizedToTriggerApplicationReview, + AccountNotAuthorizedToPollApplication, + AccountNotAuthorizedToSubmitMilestone, + AccountNotAuthorizedToPollMilestone, + AccountNotAuthorizedToTriggerMilestoneReview, ReviewBoardWeightedShapeDoesntSupportPetitionReview, ReviewBoardFlatShapeDoesntSupportThresholdReview, ApplicationMustBeUnderReviewToPoll, @@ -203,12 +169,12 @@ decl_storage! { hasher(opaque_blake2_256) BountyMapID => T::BountyId; /// Unordered set for tracking foundations as relationships b/t OrgId and OnChainTreasuryID - RegisteredFoundations get(fn registered_foundations): double_map + pub RegisteredFoundations get(fn registered_foundations): double_map hasher(blake2_128_concat) T::OrgId, hasher(blake2_128_concat) OnChainTreasuryID => bool; /// Posted bounty details - FoundationSponsoredBounties get(fn foundation_sponsored_bounties): map + pub FoundationSponsoredBounties get(fn foundation_sponsored_bounties): map hasher(opaque_blake2_256) T::BountyId => Option< BountyInformation< T::OrgId, @@ -220,12 +186,12 @@ decl_storage! { >; /// All bounty applications - BountyApplications get(fn bounty_applications): double_map + pub BountyApplications get(fn bounty_applications): double_map hasher(opaque_blake2_256) T::BountyId, hasher(opaque_blake2_256) T::BountyId => Option, T::IpfsReference, ApplicationState, T::VoteId>>>; /// All milestone submissions - MilestoneSubmissions get(fn milestone_submissions): double_map + pub MilestoneSubmissions get(fn milestone_submissions): double_map hasher(opaque_blake2_256) T::BountyId, hasher(opaque_blake2_256) T::BountyId => Option, T::AccountId, T::BountyId, MilestoneStatus>>>; } @@ -261,7 +227,10 @@ decl_module! { supervision_committee: Option>>, ) -> DispatchResult { let bounty_creator = ensure_signed(origin)?; - // TODO: AUTH + // auth + let authentication = Self::can_create_bounty(&bounty_creator, registered_organization); + ensure!(authentication, Error::::AccountNotAuthorizedToCreateBounty); + // state machine execution let bounty_identifier = Self::create_bounty( registered_organization, bank_account, @@ -291,6 +260,10 @@ decl_module! { terms_of_agreement: TermsOfAgreement, ) -> DispatchResult { let submitter = ensure_signed(origin)?; + // auth + let authentication = Self::can_submit_grant_app(&submitter, terms_of_agreement.clone()); + ensure!(authentication, Error::::NotAuthorizedToSubmitGrantApplication); + // state machine execution let new_grant_app_id = Self::submit_grant_application(submitter.clone(), bounty_id, description.clone(), total_amount, terms_of_agreement)?; Self::deposit_event(RawEvent::GrantApplicationSubmittedForBounty(submitter, bounty_id, new_grant_app_id, description, total_amount)); Ok(()) @@ -302,6 +275,10 @@ decl_module! { application_id: T::BountyId, ) -> DispatchResult { let trigger = ensure_signed(origin)?; + // auth + let authentication = Self::can_trigger_grant_app_review(&trigger, bounty_id)?; + ensure!(authentication, Error::::AccountNotAuthorizedToTriggerApplicationReview); + // state machine execution let application_state = Self::trigger_application_review(bounty_id, application_id)?; Self::deposit_event(RawEvent::ApplicationReviewTriggered(trigger, bounty_id, application_id, application_state)); Ok(()) @@ -323,7 +300,11 @@ decl_module! { bounty_id: T::BountyId, application_id: T::BountyId, ) -> DispatchResult { - let _ = ensure_signed(origin)?; + let poller = ensure_signed(origin)?; + // auth + let authentication = Self::can_poll_grant_app(&poller, bounty_id)?; + ensure!(authentication, Error::::AccountNotAuthorizedToPollApplication); + // state machine execution let app_state = Self::poll_application(bounty_id, application_id)?; Self::deposit_event(RawEvent::ApplicationPolled(bounty_id, application_id, app_state)); Ok(()) @@ -337,6 +318,10 @@ decl_module! { amount_requested: BalanceOf, ) -> DispatchResult { let submitter = ensure_signed(origin)?; + // auth + let authentication = Self::can_submit_milestone(&submitter, bounty_id, application_id)?; + ensure!(authentication, Error::::AccountNotAuthorizedToSubmitMilestone); + // state machine execution let new_milestone_id = Self::submit_milestone(submitter.clone(), bounty_id, application_id, submission_reference, amount_requested)?; Self::deposit_event(RawEvent::MilestoneSubmitted(submitter, bounty_id, application_id, new_milestone_id)); Ok(()) @@ -348,6 +333,10 @@ decl_module! { milestone_id: T::BountyId, ) -> DispatchResult { let trigger = ensure_signed(origin)?; + // auth + let authentication = Self::can_trigger_milestone_review(&trigger, bounty_id)?; + ensure!(authentication, Error::::AccountNotAuthorizedToTriggerMilestoneReview); + // state machine execution let milestone_state = Self::trigger_milestone_review(bounty_id, milestone_id)?; Self::deposit_event(RawEvent::MilestoneReviewTriggered(trigger, bounty_id, milestone_id, milestone_state)); Ok(()) @@ -370,6 +359,10 @@ decl_module! { milestone_id: T::BountyId, ) -> DispatchResult { let poller = ensure_signed(origin)?; + // auth + let authentication = Self::can_poll_milestone(&poller, bounty_id)?; + ensure!(authentication, Error::::AccountNotAuthorizedToPollMilestone); + // state machine execution let milestone_state = Self::poll_milestone(bounty_id, milestone_id)?; Self::deposit_event(RawEvent::MilestonePolled(poller, bounty_id, milestone_id, milestone_state)); Ok(()) @@ -445,12 +438,7 @@ impl RegisterFoundation, T::AccountId> for Modu } fn register_foundation_from_existing_bank(org: T::OrgId, bank: Self::BankId) -> DispatchResult { ensure!( - <::Bank as RegisterAccount< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - >>::verify_owner(bank.into(), org), + >::verify_owner(bank, org), Error::::CannotRegisterFoundationFromOrgBankRelationshipThatDNE ); >::insert(org, bank, true); @@ -519,17 +507,11 @@ impl ); // reserve `amount_reserved_for_bounty` here by calling into `bank-onchain` - let spend_reservation_id = <::Bank as ReservationMachine< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - >>::reserve_for_spend( - bank_account.into(), + let spend_reservation_id = >::reserve_for_spend( + bank_account, description.clone(), amount_reserved_for_bounty, - acceptance_committee.clone().into(), // converts ReviewBoard Into WithdrawalPermissions + acceptance_committee.org(), // reserved for who? should be the ultimate recipient? )?; // form the bounty_info let new_bounty_info = BountyInformation::new( @@ -909,14 +891,8 @@ impl // commit reserved spend for transfer before vote begins // -> this sets funds aside in case of a positive outcome, // it is not _optimistic_, it is fair to add this commitment - <::Bank as ReservationMachine< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - >>::commit_reserved_spend_for_transfer( - bounty_info.bank_account().into(), + >::commit_reserved_spend_for_transfer( + bounty_info.bank_account(), bounty_info.spend_reservation(), milestone_submission.amount(), )?; @@ -969,18 +945,12 @@ impl .ok_or(Error::::SubmissionIsNotReadyForReview)?; // commit and transfer control over capital in the same step - let new_transfer_id = <::Bank as CommitAndTransfer< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - >>::commit_and_transfer_spending_power( - bounty_info.bank_account().into(), + let new_transfer_id = >::commit_and_transfer_spending_power( + bounty_info.bank_account(), bounty_info.spend_reservation(), milestone_submission.submission(), // reason = hash of milestone submission milestone_submission.amount(), - WithdrawalPermissions::JointOrgAccount(team_org_id), + team_org_id, )?; let new_milestone_submission = milestone_submission .set_make_transfer(bounty_info.bank_account(), new_transfer_id) @@ -1020,18 +990,12 @@ impl application.spend_approved_grant(milestone_submission.amount()) { // make the transfer - let transfer_id = <::Bank as ReservationMachine< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - >>::transfer_spending_power( - bounty_info.bank_account().into(), + let transfer_id = >::transfer_spending_power( + bounty_info.bank_account(), milestone_submission.submission(), // reason = hash of milestone submission bounty_info.spend_reservation(), milestone_submission.amount(), - WithdrawalPermissions::JointOrgAccount(org_id), // uses the weighted share issuance by default to enforce payout structure + org_id, // uses the weighted share issuance by default to enforce payout structure )?; // insert updated application into storage >::insert( @@ -1065,18 +1029,12 @@ impl application.spend_approved_grant(milestone_submission.amount()) { // make the transfer - let transfer_id = <::Bank as ReservationMachine< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - >>::transfer_spending_power( - bounty_info.bank_account().into(), + let transfer_id = >::transfer_spending_power( + bounty_info.bank_account(), milestone_submission.submission(), // reason = hash of milestone submission bounty_info.spend_reservation(), milestone_submission.amount(), - WithdrawalPermissions::JointOrgAccount(org_id), // uses the weighted share issuance by default to enforce payout structure + org_id, // uses the weighted share issuance by default to enforce payout structure )?; // insert updated application into storage >::insert( @@ -1113,18 +1071,12 @@ impl application.spend_approved_grant(milestone_submission.amount()) { // make the transfer - let transfer_id = <::Bank as ReservationMachine< - T::OrgId, - T::AccountId, - WithdrawalPermissions, - BalanceOf, - T::IpfsReference, - >>::transfer_spending_power( - bounty_info.bank_account().into(), + let transfer_id = >::transfer_spending_power( + bounty_info.bank_account(), milestone_submission.submission(), // reason = hash of milestone submission bounty_info.spend_reservation(), milestone_submission.amount(), - WithdrawalPermissions::JointOrgAccount(org_id), + org_id, )?; let new_milestone_submission = milestone_submission .set_make_transfer(bounty_info.bank_account(), transfer_id) @@ -1149,3 +1101,105 @@ impl } } } + +/// Default permissions logic, could save some lookups by putting this with module +/// logic in the other traits impls but this is more readable +impl + BountyPermissions< + T::OrgId, + TermsOfAgreement, + T::AccountId, + T::BountyId, + > for Module +{ + // TODO: consider adding `can_register_foundation` + fn can_create_bounty(who: &T::AccountId, hosting_org: T::OrgId) -> bool { + // any member of the foundation can post a bounty + >::is_member_of_group(hosting_org, who) + } + fn can_submit_grant_app( + who: &T::AccountId, + terms: TermsOfAgreement, + ) -> bool { + // ensure that the submitter is one of the accounts in the terms of agreement + terms + .weighted() + .into_iter() + .any(|(account, _)| &account == who) + } + fn can_trigger_grant_app_review( + who: &T::AccountId, + bounty_id: T::BountyId, + ) -> Result { + let bounty_info = >::get(bounty_id) + .ok_or(Error::::CannotReviewApplicationIfBountyDNE)?; + // ensure that the who is a member of the bounty supervising committee + Ok(>::is_member_of_group( + bounty_info.acceptance_committee().org(), + who, + )) + // && ensure not a member of the applying team? That's an interesting check to at least add for demonstration purposes + } + fn can_poll_grant_app( + who: &T::AccountId, + bounty_id: T::BountyId, + ) -> Result { + let bounty_info = >::get(bounty_id) + .ok_or(Error::::CannotReviewApplicationIfBountyDNE)?; + // ensure that the who is a member of the bounty supervising committee + Ok(>::is_member_of_group( + bounty_info.acceptance_committee().org(), + who, + )) + // || ensure not a member of the applying team? That's an interesting loosening to at least add for demonstration purposes + } + // save a lookup by passing in the object instead of the identifier to get the object from storage + fn can_submit_milestone( + who: &T::AccountId, + bounty_id: T::BountyId, + application_id: T::BountyId, + ) -> Result { + let _ = >::get(bounty_id) + .ok_or(Error::::CannotSubmitMilestoneIfBountyDNE)?; + let team_org = >::get(bounty_id, application_id) + .ok_or(Error::::CannotSubmitMilestoneIfApplicationDNE)? + .state() + .approved_and_live() + .ok_or(Error::::CannotSubmitMilestoneIfApplicationNotApprovedAndLive)?; + Ok(>::is_member_of_group(team_org.org(), who)) + } + fn can_poll_milestone( + who: &T::AccountId, + bounty_id: T::BountyId, + ) -> Result { + let bounty_info = >::get(bounty_id) + .ok_or(Error::::CannotReviewApplicationIfBountyDNE)?; + let milestone_review_board = if let Some(board) = bounty_info.supervision_committee() { + board + } else { + bounty_info.acceptance_committee() + }; + // ensure that the who is a member of the review board + Ok(>::is_member_of_group( + milestone_review_board.org(), + who, + )) + } + fn can_trigger_milestone_review( + who: &T::AccountId, + bounty_id: T::BountyId, + ) -> Result { + let bounty_info = >::get(bounty_id) + .ok_or(Error::::CannotReviewApplicationIfBountyDNE)?; + let milestone_review_board = if let Some(board) = bounty_info.supervision_committee() { + board + } else { + bounty_info.acceptance_committee() + }; + // ensure that the who is a member of the review board + Ok(>::is_member_of_group( + milestone_review_board.org(), + who, + )) + } +} diff --git a/modules/bounty/src/tests.rs b/modules/bounty/src/tests.rs index ed57007..bf26ef7 100644 --- a/modules/bounty/src/tests.rs +++ b/modules/bounty/src/tests.rs @@ -101,9 +101,7 @@ parameter_types! { } impl Trait for TestRuntime { type Event = TestEvent; - type Currency = Balances; type BountyId = u64; - type Bank = Bank; type MinimumBountyCollateralRatio = MinimumBountyCollateralRatio; type BountyLowerBound = BountyLowerBound; } diff --git a/modules/org/src/lib.rs b/modules/org/src/lib.rs index 794d6b9..d6e6999 100644 --- a/modules/org/src/lib.rs +++ b/modules/org/src/lib.rs @@ -12,10 +12,10 @@ use util::{ organization::{Organization, OrganizationSource}, share::{ShareProfile, SimpleShareGenesis}, traits::{ - AccessGenesis, ChainSudoPermissions, ChangeGroupMembership, GenerateUniqueID, GetGroup, - GetGroupSize, GroupMembership, IDIsAvailable, LockProfile, - OrganizationSupervisorPermissions, RegisterOrganization, RemoveOrganization, - ReserveProfile, ShareInformation, ShareIssuance, VerifyShape, + AccessGenesis, ChainSudoPermissions, GenerateUniqueID, GetGroup, GetGroupSize, + GroupMembership, IDIsAvailable, LockProfile, OrganizationSupervisorPermissions, + RegisterOrganization, RemoveOrganization, ReserveProfile, ShareInformation, ShareIssuance, + VerifyShape, }, // AccessProfile, SeededGenerateUniqueID }; @@ -114,11 +114,8 @@ decl_error! { pub enum Error for Module { UnAuthorizedSwapSudoRequest, NoExistingSudoKey, - NotAuthorizedToChangeMembership, OrganizationMustExistToClearSupervisor, OrganizationMustExistToPutSupervisor, - CannotAddAnExistingMemberToOrg, - CannotRemoveNonMemberFromOrg, CannotBurnMoreThanTotalIssuance, NotEnoughSharesToSatisfyBurnRequest, IssuanceCannotGoNegative, @@ -159,23 +156,23 @@ decl_storage! { OrgIdNonce get(fn org_id_counter): T::OrgId; /// The total number of organizations registered at any given time - OrganizationCounter get(fn organization_counter): u32; + pub OrganizationCounter get(fn organization_counter): u32; /// The main storage item for Organization registration - OrganizationStates get(fn organization_states): map + pub OrganizationStates get(fn organization_states): map hasher(blake2_128_concat) T::OrgId => Option>; /// The map to track organizational membership - Members get(fn members): double_map + pub Members get(fn members): double_map hasher(blake2_128_concat) T::OrgId, hasher(blake2_128_concat) T::AccountId => Option>; /// Total number of outstanding shares that express relative ownership in group - TotalIssuance get(fn total_issuance): map + pub TotalIssuance get(fn total_issuance): map hasher(opaque_blake2_256) T::OrgId => T::Shares; /// The size for each organization - OrganizationSize get(fn organization_size): map hasher(opaque_blake2_256) T::OrgId => u32; + pub OrganizationSize get(fn organization_size): map hasher(opaque_blake2_256) T::OrgId => u32; } add_extra_genesis { config(first_organization_supervisor): T::AccountId; @@ -236,63 +233,6 @@ decl_module! { Self::deposit_event(RawEvent::NewWeightedOrganizationRegistered(caller, new_id, constitution, wm_cpy.total())); Ok(()) } - #[weight = 0] - fn add_new_member_to_org( - origin, - organization: T::OrgId, - new_member: T::AccountId, - shares_issued: T::Shares, - ) -> DispatchResult { - let caller = ensure_signed(origin)?; - let authentication: bool = Self::is_sudo_key(&caller) || Self::is_organization_supervisor(organization, &caller); - ensure!(authentication, Error::::NotAuthorizedToChangeMembership); - - Self::add_member_to_org(organization, new_member.clone(), false)?; - Self::deposit_event(RawEvent::NewMemberAddedToOrg(organization, new_member, T::Shares::zero())); - Ok(()) - } - #[weight = 0] - fn remove_old_member_from_org( - origin, - organization: T::OrgId, - old_member: T::AccountId, - ) -> DispatchResult { - let caller = ensure_signed(origin)?; - let authentication: bool = Self::is_sudo_key(&caller) || Self::is_organization_supervisor(organization, &caller); - ensure!(authentication, Error::::NotAuthorizedToChangeMembership); - - Self::remove_member_from_org(organization, old_member.clone(), false)?; - Self::deposit_event(RawEvent::OldMemberRemovedFromOrg(organization, old_member, T::Shares::zero())); - Ok(()) - } - #[weight = 0] - fn add_new_members_to_org( - origin, - organization: T::OrgId, - new_members: Vec, - ) -> DispatchResult { - let caller = ensure_signed(origin)?; - let authentication: bool = Self::is_sudo_key(&caller) || Self::is_organization_supervisor(organization, &caller); - ensure!(authentication, Error::::NotAuthorizedToChangeMembership); - - Self::batch_add_members_to_org(organization, new_members)?; - Self::deposit_event(RawEvent::BatchMemberAdditionForOrg(caller, organization, T::Shares::zero())); - Ok(()) - } - #[weight = 0] - fn remove_old_members_from_org( - origin, - organization: T::OrgId, - old_members: Vec, - ) -> DispatchResult { - let caller = ensure_signed(origin)?; - let authentication: bool = Self::is_sudo_key(&caller) || Self::is_organization_supervisor(organization, &caller); - ensure!(authentication, Error::::NotAuthorizedToChangeMembership); - - Self::batch_remove_members_from_org(organization, old_members)?; - Self::deposit_event(RawEvent::BatchMemberRemovalForOrg(caller, organization, T::Shares::zero())); - Ok(()) - } /// Share Issuance Runtime Methods #[weight = 0] fn issue_shares(origin, organization: T::OrgId, who: T::AccountId, shares: T::Shares) -> DispatchResult { @@ -409,8 +349,8 @@ decl_module! { || unreserver == who; ensure!(authentication, Error::::NotAuthorizedToUnReserveShares); - let amount_reserved = Self::unreserve(organization, &who, None)?; - Self::deposit_event(RawEvent::SharesUnReserved(organization, who, amount_reserved)); + let amount_unreserved = Self::unreserve(organization, &who, None)?; + Self::deposit_event(RawEvent::SharesUnReserved(organization, who, amount_unreserved)); Ok(()) } } @@ -507,7 +447,11 @@ impl RegisterOrganization fo match src { OrganizationSource::Accounts(accounts) => { // batch_add (flat membership group) - Self::batch_add_members_to_org(org_id, accounts)?; + let weighted_acc = accounts + .into_iter() + .map(|acc| (acc, 1u32.into())) + .collect::>(); + Self::batch_issue(org_id, weighted_acc.into())?; Ok(Organization::new(supervisor, parent_id, value_constitution)) } OrganizationSource::AccountsWeighted(weighted_accounts) => { @@ -574,76 +518,6 @@ impl RemoveOrganization for Module { } } -impl ChangeGroupMembership for Module { - fn add_member_to_org( - org_id: T::OrgId, - new_member: T::AccountId, - batch: bool, - ) -> DispatchResult { - // prevent adding duplicate members - ensure!( - >::get(org_id, &new_member).is_none(), - Error::::CannotAddAnExistingMemberToOrg - ); - if !batch { - let new_organization_size = >::get(org_id) + 1u32; - >::insert(org_id, new_organization_size); - } - // default 0 share profile inserted for this method - >::insert(org_id, new_member, ShareProfile::default()); - Ok(()) - } - fn remove_member_from_org( - org_id: T::OrgId, - old_member: T::AccountId, - batch: bool, - ) -> DispatchResult { - // prevent removal of non-member - let current_profile = >::get(org_id, &old_member) - .ok_or(Error::::CannotRemoveNonMemberFromOrg)?; - // update issuance if it changes due to this removal - if current_profile.total() > T::Shares::zero() { - let new_issuance = >::get(org_id) - .checked_sub(¤t_profile.total()) - .ok_or(Error::::IssuanceGoesNegativeWhileRemovingMember)?; - >::insert(org_id, new_issuance); - } - if !batch { - let new_organization_size = >::get(org_id).saturating_sub(1u32); - >::insert(org_id, new_organization_size); - } - >::remove(org_id, old_member); - Ok(()) - } - // WARNING: the vector fed as inputs to the following methods must have NO duplicates - fn batch_add_members_to_org( - org_id: T::OrgId, - new_members: Vec, - ) -> DispatchResult { - let size_increase: u32 = new_members.len() as u32; - let new_organization_size: u32 = - >::get(org_id).saturating_add(size_increase); - >::insert(org_id, new_organization_size); - new_members - .into_iter() - .map(|member| Self::add_member_to_org(org_id, member, true)) - .collect::() - } - fn batch_remove_members_from_org( - org_id: T::OrgId, - old_members: Vec, - ) -> DispatchResult { - let size_decrease: u32 = old_members.len() as u32; - let new_organization_size: u32 = - >::get(org_id).saturating_sub(size_decrease); - >::insert(org_id, new_organization_size); - old_members - .into_iter() - .map(|member| Self::remove_member_from_org(org_id, member, true)) - .collect::() - } -} - impl GetGroup for Module { fn get_group(organization: T::OrgId) -> Option> { if !Self::id_is_available(organization) { diff --git a/modules/util/src/bank.rs b/modules/util/src/bank.rs index fbad3da..9e62420 100644 --- a/modules/util/src/bank.rs +++ b/modules/util/src/bank.rs @@ -41,64 +41,9 @@ pub enum BankMapID { InternalTransfer, } -#[derive(PartialEq, Eq, Clone, Encode, Decode, sp_runtime::RuntimeDebug)] -/// The simplest `GovernanceConfig` -/// - has no magnitude context and is limited for that reason -/// - future version will use revocable representative governance -pub enum WithdrawalPermissions { - // any of two accounts can reserve free capital for spending - TwoAccounts(AccountId, AccountId), - // withdrawal permissions restricted by weighted membership in organization - // -> sudo might have additional power, depends on impl - JointOrgAccount(Id), -} - -impl + Copy, AccountId> Default - for WithdrawalPermissions -{ - fn default() -> WithdrawalPermissions { - // the chain's dev account? - WithdrawalPermissions::JointOrgAccount(OrgId::zero()) - } -} - -impl + Copy, AccountId> - WithdrawalPermissions -{ - pub fn extract_org_id(&self) -> Option { - match self { - WithdrawalPermissions::JointOrgAccount(org_id) => Some(*org_id), - _ => None, - } - } -} -use crate::bounty::{ReviewBoard, TeamID}; -impl< - OrgId: Codec + PartialEq + Zero + From + Copy, - AccountId: PartialEq, - Hash: Clone, - Threshold: Clone, - > From> - for WithdrawalPermissions -{ - fn from( - other: ReviewBoard, - ) -> WithdrawalPermissions { - WithdrawalPermissions::JointOrgAccount(other.org()) - } -} -impl + Copy, AccountId: Clone + PartialEq> - From> for WithdrawalPermissions -{ - fn from(other: TeamID) -> WithdrawalPermissions { - WithdrawalPermissions::JointOrgAccount(other.org()) - } -} - #[derive(PartialEq, Eq, Clone, Encode, Decode, sp_runtime::RuntimeDebug)] /// This is the state for an OnChainBankId, associated with each bank registered in the runtime -pub struct BankState + Copy, GovernanceConfig, Currency> -{ +pub struct BankState + Copy, Currency> { // Registered organization identifier owners: OrgId, // Free capital, available for spends @@ -107,16 +52,13 @@ pub struct BankState + Copy, Governa reserved: Currency, // Operator of the organization, might be required to be a suborg or `owners` or not // -> power should be restricted by actions made by the `self.owners` - operators: GovernanceConfig, + operators: Option, } -impl< - OrgId: Codec + PartialEq + Zero + From + Copy, - GovernanceConfig: Clone + PartialEq, - Currency: Zero + AtLeast32Bit + Clone, - > BankState +impl + Copy, Currency: Zero + AtLeast32Bit + Clone> + BankState { - pub fn new_from_deposit(owners: OrgId, amount: Currency, operators: GovernanceConfig) -> Self { + pub fn new_from_deposit(owners: OrgId, amount: Currency, operators: Option) -> Self { BankState { owners, free: amount, @@ -133,22 +75,25 @@ impl< pub fn reserved(&self) -> Currency { self.reserved.clone() } - pub fn operators(&self) -> GovernanceConfig { - self.operators.clone() + pub fn operators(&self) -> Option { + self.operators } pub fn is_owners(&self, org: OrgId) -> bool { org == self.owners } - pub fn is_operator(&self, cmp_owner: GovernanceConfig) -> bool { - cmp_owner == self.operators.clone() + pub fn is_operator(&self, org: OrgId) -> bool { + if let Some(op) = self.operators { + op == org + } else { + false + } } } impl< OrgId: Codec + PartialEq + Zero + From + Copy, - GovernanceConfig: Clone + PartialEq, Currency: Zero + AtLeast32Bit + Clone + sp_std::ops::Add + sp_std::ops::Sub, - > FreeToReserved for BankState + > FreeToReserved for BankState { fn move_from_free_to_reserved(&self, amount: Currency) -> Option { if self.free() >= amount { @@ -170,9 +115,8 @@ impl< impl< OrgId: Codec + PartialEq + Zero + From + Copy, - GovernanceConfig: Clone + PartialEq, Currency: Zero + AtLeast32Bit + Clone + sp_std::ops::Add, - > GetBalance for BankState + > GetBalance for BankState { fn total_free_funds(&self) -> Currency { self.free() @@ -185,11 +129,8 @@ impl< } } -impl< - OrgId: Codec + PartialEq + Zero + From + Copy, - GovernanceConfig: Clone + PartialEq, - Currency: Zero + AtLeast32Bit + Clone, - > DepositSpendOps for BankState +impl + Copy, Currency: Zero + AtLeast32Bit + Clone> + DepositSpendOps for BankState { // infallible fn deposit_into_free(&self, amount: Currency) -> Self { diff --git a/modules/util/src/traits.rs b/modules/util/src/traits.rs index af1e55e..6dfb7d4 100644 --- a/modules/util/src/traits.rs +++ b/modules/util/src/traits.rs @@ -47,15 +47,6 @@ pub trait GetGroupSize { pub trait GroupMembership: GetGroupSize { fn is_member_of_group(org_id: OrgId, who: &AccountId) -> bool; } - -/// All changes to the organizational membership are infallible -pub trait ChangeGroupMembership: GroupMembership { - fn add_member_to_org(org_id: OrgId, new_member: AccountId, batch: bool) -> DispatchResult; - fn remove_member_from_org(org_id: OrgId, old_member: AccountId, batch: bool) -> DispatchResult; - /// WARNING: the vector fed as inputs to the following methods must have NO duplicates - fn batch_add_members_to_org(org_id: OrgId, new_members: Vec) -> DispatchResult; - fn batch_remove_members_from_org(org_id: OrgId, old_members: Vec) -> DispatchResult; -} pub trait GetGroup { fn get_group(organization: OrgId) -> Option>; } @@ -277,28 +268,28 @@ pub trait OnChainBank { type TreasuryId: Clone + From; type AssociatedId: Codec + Copy + PartialEq + From + Zero; } -pub trait RegisterAccount: OnChainBank { +pub trait RegisterAccount: OnChainBank { // requires a deposit of some size above the minimum and returns the OnChainTreasuryID fn register_account( owners: OrgId, from: AccountId, amount: Currency, - owner_s: GovernanceConfig, + operators: Option, ) -> Result; fn verify_owner(bank_id: Self::TreasuryId, org: OrgId) -> bool; } // people should be eventually able to solicit loans from others to SEED a bank account but they cede some or all of the control... -pub trait CalculateOwnership: - RegisterAccount +pub trait CalculateOwnership: + RegisterAccount { fn calculate_proportion_ownership_for_account( account: AccountId, - group: GovernanceConfig, + group: OrgId, ) -> Result; fn calculate_proportional_amount_for_account( amount: Currency, account: AccountId, - group: GovernanceConfig, + group: OrgId, ) -> Result; } @@ -345,8 +336,8 @@ pub trait CheckBankBalances: OnChainBank + DepositsAndSpends fn calculate_total_bank_balance_from_balances(bank_id: Self::TreasuryId) -> Option; } -pub trait DepositIntoBank: - RegisterAccount + DepositsAndSpends +pub trait DepositIntoBank: + RegisterAccount + DepositsAndSpends { // get the bank corresponding to bank_id call infallible deposit // - only fails if `from` doesn't have enough Currency @@ -358,14 +349,11 @@ pub trait DepositIntoBank: ) -> Result; // returns DepositId } -pub trait DefaultBankPermissions: +pub trait DefaultBankPermissions: DepositsAndSpends + OnChainBank { fn can_register_account(account: AccountId, on_behalf_of: OrgId) -> bool; - fn withdrawal_permissions_satisfy_org_standards( - org: OrgId, - withdrawal_permissions: WithdrawalPermissions, - ) -> bool; + fn operator_satisfies_requirements(org: OrgId, operator: OrgId) -> bool; fn can_reserve_for_spend( account: AccountId, bank: Self::TreasuryId, @@ -395,7 +383,7 @@ pub trait DefaultBankPermissions: - RegisterAccount +pub trait ReservationMachine: + RegisterAccount { fn reserve_for_spend( bank_id: Self::TreasuryId, reason: Hash, amount: Currency, // acceptance committee for approving set aside spends below the amount - controller: GovernanceConfig, + controller: OrgId, ) -> Result; fn commit_reserved_spend_for_transfer( bank_id: Self::TreasuryId, @@ -438,12 +426,12 @@ pub trait ReservationMachine reservation_id: Self::AssociatedId, amount: Currency, // move control of funds to new outer group which can reserve or withdraw directly - new_controller: GovernanceConfig, + new_controller: OrgId, ) -> Result; // returns transfer_id } -pub trait CommitAndTransfer: - ReservationMachine +pub trait CommitAndTransfer: + ReservationMachine { // in one step fn commit_and_transfer_spending_power( @@ -451,12 +439,12 @@ pub trait CommitAndTransfer: reservation_id: Self::AssociatedId, reason: Hash, amount: Currency, - new_controller: GovernanceConfig, + new_controller: OrgId, ) -> Result; } -pub trait ExecuteSpends: - OnChainBank + ReservationMachine +pub trait ExecuteSpends: + OnChainBank + ReservationMachine { fn spend_from_free( from_bank_id: Self::TreasuryId, @@ -674,7 +662,7 @@ pub trait SubmitMilestone; type MilestoneState; fn submit_milestone( - caller: AccountId, // must be from the team, maybe check sudo || flat_org_member + caller: AccountId, bounty_id: BountyId, application_id: BountyId, submission_reference: Hash, @@ -695,3 +683,28 @@ pub trait SubmitMilestone Result; } + +// We could remove`can_submit_grant_app` or `can_submit_milestone` because both of these paths log the submitter +// in the associated state anyway so we might as well pass the caller into the methods that do this logic and +// perform any context-based authentication there, but readability is more important at this point +pub trait BountyPermissions: + UseTermsOfAgreement +{ + fn can_create_bounty(who: &AccountId, hosting_org: OrgId) -> bool; + fn can_submit_grant_app(who: &AccountId, terms: TermsOfAgreement) -> bool; + fn can_trigger_grant_app_review( + who: &AccountId, + bounty_id: BountyId, + ) -> Result; + fn can_poll_grant_app(who: &AccountId, bounty_id: BountyId) -> Result; + fn can_submit_milestone( + who: &AccountId, + bounty_id: BountyId, + application_id: BountyId, + ) -> Result; + fn can_poll_milestone(who: &AccountId, bounty_id: BountyId) -> Result; + fn can_trigger_milestone_review( + who: &AccountId, + bounty_id: BountyId, + ) -> Result; +} diff --git a/modules/vote/src/lib.rs b/modules/vote/src/lib.rs index 477e673..8b79578 100644 --- a/modules/vote/src/lib.rs +++ b/modules/vote/src/lib.rs @@ -160,13 +160,13 @@ decl_module! { origin, topic: Option, organization: T::OrgId, - ends: Option, + duration: Option, ) -> DispatchResult { let vote_creator = ensure_signed(origin)?; // default authentication is organization supervisor or sudo key let authentication: bool = >::is_organization_supervisor(organization, &vote_creator) || >::is_sudo_key(&vote_creator); ensure!(authentication, Error::::NotAuthorizedToCreateVoteForOrganization); - let new_vote_id = Self::open_unanimous_consent(topic, organization, ends)?; + let new_vote_id = Self::open_unanimous_consent(topic, organization, duration)?; // emit event Self::deposit_event(RawEvent::NewVoteStarted(vote_creator, organization, new_vote_id)); Ok(()) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7f445d1..0e6cbbf 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -338,7 +338,7 @@ impl vote::Trait for Runtime { pub use bank; parameter_types! { // minimum deposit to register an on-chain bank - pub const MinimumInitialDeposit: u64 = 5; + pub const MinimumInitialDeposit: u128 = 5; } impl bank::Trait for Runtime { type Event = Event; @@ -349,13 +349,11 @@ impl bank::Trait for Runtime { pub use bounty; parameter_types! { pub const MinimumBountyCollateralRatio: Permill = Permill::from_percent(20); - pub const BountyLowerBound: u64 = 10; + pub const BountyLowerBound: u128 = 10; } impl bounty::Trait for Runtime { type Event = Event; - type Currency = Balances; type BountyId = u64; - type Bank = Bank; type MinimumBountyCollateralRatio = MinimumBountyCollateralRatio; type BountyLowerBound = BountyLowerBound; }