use std::{collections::HashSet, marker::PhantomData};
use anyhow::{anyhow, ensure};
use codec::{Decode, Encode};
use crate::{
account_from_keypair, aleph_runtime::RuntimeCall, api, api::runtime_types, connections::TxInfo,
sp_core::blake2_256, sp_runtime::traits::TrailingZeroInput, sp_weights::weight_v2::Weight,
AccountId, Balance, BlockHash, BlockNumber, ConnectionApi, SignedConnectionApi, TxStatus,
};
pub type CallHash = [u8; 32];
pub type Call = RuntimeCall;
pub type MultisigThreshold = u16;
pub type Timepoint = runtime_types::pallet_multisig::Timepoint<BlockNumber>;
pub type Multisig = runtime_types::pallet_multisig::Multisig<BlockNumber, Balance, AccountId>;
pub const DEFAULT_MAX_WEIGHT: Weight = Weight::new(500_000_000, 0);
#[async_trait::async_trait]
pub trait MultisigUserApi {
async fn as_multi_threshold_1(
&self,
other_signatories: Vec<AccountId>,
call: Call,
status: TxStatus,
) -> anyhow::Result<TxInfo>;
async fn as_multi(
&self,
threshold: MultisigThreshold,
other_signatories: Vec<AccountId>,
timepoint: Option<Timepoint>,
max_weight: Weight,
call: Call,
status: TxStatus,
) -> anyhow::Result<TxInfo>;
async fn approve_as_multi(
&self,
threshold: MultisigThreshold,
other_signatories: Vec<AccountId>,
timepoint: Option<Timepoint>,
max_weight: Weight,
call_hash: CallHash,
status: TxStatus,
) -> anyhow::Result<TxInfo>;
async fn cancel_as_multi(
&self,
threshold: MultisigThreshold,
other_signatories: Vec<AccountId>,
timepoint: Timepoint,
call_hash: CallHash,
status: TxStatus,
) -> anyhow::Result<TxInfo>;
}
#[async_trait::async_trait]
impl<S: SignedConnectionApi> MultisigUserApi for S {
async fn as_multi_threshold_1(
&self,
other_signatories: Vec<AccountId>,
call: Call,
status: TxStatus,
) -> anyhow::Result<TxInfo> {
let tx = api::tx()
.multisig()
.as_multi_threshold_1(other_signatories, call);
self.send_tx(tx, status).await
}
async fn as_multi(
&self,
threshold: MultisigThreshold,
other_signatories: Vec<AccountId>,
timepoint: Option<Timepoint>,
max_weight: Weight,
call: Call,
status: TxStatus,
) -> anyhow::Result<TxInfo> {
let tx = api::tx().multisig().as_multi(
threshold,
other_signatories,
timepoint,
call,
max_weight,
);
self.send_tx(tx, status).await
}
async fn approve_as_multi(
&self,
threshold: MultisigThreshold,
other_signatories: Vec<AccountId>,
timepoint: Option<Timepoint>,
max_weight: Weight,
call_hash: CallHash,
status: TxStatus,
) -> anyhow::Result<TxInfo> {
let tx = api::tx().multisig().approve_as_multi(
threshold,
other_signatories,
timepoint,
call_hash,
max_weight,
);
self.send_tx(tx, status).await
}
async fn cancel_as_multi(
&self,
threshold: MultisigThreshold,
other_signatories: Vec<AccountId>,
timepoint: Timepoint,
call_hash: CallHash,
status: TxStatus,
) -> anyhow::Result<TxInfo> {
let tx = api::tx().multisig().cancel_as_multi(
threshold,
other_signatories,
timepoint,
call_hash,
);
self.send_tx(tx, status).await
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct MultisigParty {
signatories: Vec<AccountId>,
threshold: MultisigThreshold,
}
impl MultisigParty {
pub fn new(signatories: &[AccountId], threshold: MultisigThreshold) -> anyhow::Result<Self> {
let mut sorted_signatories = signatories.to_vec();
sorted_signatories.sort();
sorted_signatories.dedup();
ensure!(
sorted_signatories.len() > 1,
"There must be at least 2 different signatories"
);
ensure!(
sorted_signatories.len() >= threshold as usize,
"Threshold must not be greater than the number of unique signatories"
);
ensure!(
threshold >= 2,
"Threshold must be at least 2 - for threshold 1, use `as_multi_threshold_1`"
);
Ok(Self {
signatories: sorted_signatories,
threshold,
})
}
pub fn account(&self) -> AccountId {
let entropy =
(b"modlpy/utilisuba", &self.signatories, &self.threshold).using_encoded(blake2_256);
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
.expect("infinite length input; no invalid inputs for type; qed")
}
}
#[async_trait::async_trait]
pub trait MultisigApiExt {
async fn get_timepoint(
&self,
party_account: &AccountId,
call_hash: &CallHash,
block_hash: Option<BlockHash>,
) -> Timepoint;
}
#[async_trait::async_trait]
impl<C: ConnectionApi> MultisigApiExt for C {
async fn get_timepoint(
&self,
party_account: &AccountId,
call_hash: &CallHash,
block_hash: Option<BlockHash>,
) -> Timepoint {
let multisigs = api::storage()
.multisig()
.multisigs(party_account, call_hash);
let Multisig { when, .. } = self.get_storage_entry(&multisigs, block_hash).await;
when
}
}
pub trait ContextState {}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Ongoing {}
impl ContextState for Ongoing {}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Closed {}
impl ContextState for Closed {}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Context<S: ContextState> {
party: MultisigParty,
author: AccountId,
timepoint: Timepoint,
max_weight: Weight,
call: Option<Call>,
call_hash: CallHash,
approvers: HashSet<AccountId>,
_phantom: PhantomData<S>,
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum ContextAfterUse {
Ongoing(Context<Ongoing>),
Closed(Context<Closed>),
}
impl Context<Ongoing> {
fn new(
party: MultisigParty,
author: AccountId,
timepoint: Timepoint,
max_weight: Weight,
call: Option<Call>,
call_hash: CallHash,
) -> Self {
Self {
party,
author: author.clone(),
timepoint,
max_weight,
call,
call_hash,
approvers: HashSet::from([author]),
_phantom: PhantomData,
}
}
pub fn change_max_weight(&mut self, max_weight: Weight) {
self.max_weight = max_weight;
}
fn set_call(&mut self, call: Call) -> anyhow::Result<()> {
ensure!(
self.call_hash == compute_call_hash(&call),
"Call doesn't match to the registered hash"
);
self.call = Some(call);
Ok(())
}
fn add_approval(mut self, approver: AccountId) -> ContextAfterUse {
self.approvers.insert(approver);
if self.call.is_some() && self.approvers.len() >= (self.party.threshold as usize) {
ContextAfterUse::Closed(self.close())
} else {
ContextAfterUse::Ongoing(self)
}
}
fn close(self) -> Context<Closed> {
Context::<Closed> {
party: self.party,
author: self.author,
timepoint: self.timepoint,
max_weight: self.max_weight,
call: self.call,
call_hash: self.call_hash,
approvers: self.approvers,
_phantom: Default::default(),
}
}
}
impl Context<Closed> {
pub fn party(&self) -> &MultisigParty {
&self.party
}
pub fn author(&self) -> &AccountId {
&self.author
}
pub fn timepoint(&self) -> &Timepoint {
&self.timepoint
}
pub fn max_weight(&self) -> &Weight {
&self.max_weight
}
pub fn call(&self) -> &Option<Call> {
&self.call
}
pub fn call_hash(&self) -> CallHash {
self.call_hash
}
pub fn approvers(&self) -> &HashSet<AccountId> {
&self.approvers
}
}
#[async_trait::async_trait]
pub trait MultisigContextualApi {
async fn initiate(
&self,
party: &MultisigParty,
max_weight: &Weight,
call_hash: CallHash,
status: TxStatus,
) -> anyhow::Result<(TxInfo, Context<Ongoing>)>;
async fn initiate_with_call(
&self,
party: &MultisigParty,
max_weight: &Weight,
call: Call,
status: TxStatus,
) -> anyhow::Result<(TxInfo, Context<Ongoing>)>;
async fn approve(
&self,
context: Context<Ongoing>,
status: TxStatus,
) -> anyhow::Result<(TxInfo, ContextAfterUse)>;
async fn approve_with_call(
&self,
context: Context<Ongoing>,
call: Option<Call>,
status: TxStatus,
) -> anyhow::Result<(TxInfo, ContextAfterUse)>;
async fn cancel(
&self,
context: Context<Ongoing>,
status: TxStatus,
) -> anyhow::Result<(TxInfo, Context<Closed>)>;
}
#[async_trait::async_trait]
impl<S: SignedConnectionApi> MultisigContextualApi for S {
async fn initiate(
&self,
party: &MultisigParty,
max_weight: &Weight,
call_hash: CallHash,
status: TxStatus,
) -> anyhow::Result<(TxInfo, Context<Ongoing>)> {
let other_signatories = ensure_signer_in_party(self, party)?;
let tx_info = self
.approve_as_multi(
party.threshold,
other_signatories,
None,
max_weight.clone(),
call_hash,
status,
)
.await?;
let timepoint = self
.get_timepoint(&party.account(), &call_hash, Some(tx_info.block_hash))
.await;
Ok((
tx_info,
Context::new(
party.clone(),
self.account_id().clone(),
timepoint,
max_weight.clone(),
None,
call_hash,
),
))
}
async fn initiate_with_call(
&self,
party: &MultisigParty,
max_weight: &Weight,
call: Call,
status: TxStatus,
) -> anyhow::Result<(TxInfo, Context<Ongoing>)> {
let other_signatories = ensure_signer_in_party(self, party)?;
let tx_info = self
.as_multi(
party.threshold,
other_signatories,
None,
max_weight.clone(),
call.clone(),
status,
)
.await?;
let call_hash = compute_call_hash(&call);
let timepoint = self
.get_timepoint(&party.account(), &call_hash, Some(tx_info.block_hash))
.await;
Ok((
tx_info,
Context::new(
party.clone(),
self.account_id().clone(),
timepoint,
max_weight.clone(),
Some(call.clone()),
call_hash,
),
))
}
async fn approve(
&self,
context: Context<Ongoing>,
status: TxStatus,
) -> anyhow::Result<(TxInfo, ContextAfterUse)> {
let other_signatories = ensure_signer_in_party(self, &context.party)?;
self.approve_as_multi(
context.party.threshold,
other_signatories,
Some(context.timepoint.clone()),
context.max_weight.clone(),
context.call_hash,
status,
)
.await
.map(|tx_info| (tx_info, context.add_approval(self.account_id().clone())))
}
async fn approve_with_call(
&self,
mut context: Context<Ongoing>,
call: Option<Call>,
status: TxStatus,
) -> anyhow::Result<(TxInfo, ContextAfterUse)> {
let other_signatories = ensure_signer_in_party(self, &context.party)?;
let call = match (call.as_ref(), context.call.as_ref()) {
(None, None) => Err(anyhow!(
"Call wasn't provided earlier - you must pass it now"
)),
(None, Some(call)) => Ok(call),
(Some(call), None) => {
context.set_call(call.clone())?;
Ok(call)
}
(Some(saved_call), Some(new_call)) => {
ensure!(
saved_call == new_call,
"The call is different that the one used previously"
);
Ok(new_call)
}
}?;
self.as_multi(
context.party.threshold,
other_signatories,
Some(context.timepoint.clone()),
context.max_weight.clone(),
call.clone(),
status,
)
.await
.map(|tx_info| (tx_info, context.add_approval(self.account_id().clone())))
}
async fn cancel(
&self,
context: Context<Ongoing>,
status: TxStatus,
) -> anyhow::Result<(TxInfo, Context<Closed>)> {
let other_signatories = ensure_signer_in_party(self, &context.party)?;
ensure!(
*self.account_id() == context.author,
"Only the author can cancel multisig aggregation"
);
let tx_info = self
.cancel_as_multi(
context.party.threshold,
other_signatories,
context.timepoint.clone(),
context.call_hash,
status,
)
.await?;
Ok((tx_info, context.close()))
}
}
pub fn compute_call_hash(call: &Call) -> CallHash {
call.using_encoded(blake2_256)
}
fn ensure_signer_in_party<S: SignedConnectionApi>(
conn: &S,
party: &MultisigParty,
) -> anyhow::Result<Vec<AccountId>> {
let signer_account = account_from_keypair(conn.signer().signer());
if let Ok(index) = party.signatories.binary_search(&signer_account) {
let mut other_signatories = party.signatories.clone();
other_signatories.remove(index);
Ok(other_signatories)
} else {
Err(anyhow!("Connection should be signed by a party member"))
}
}