diff --git a/src/rpc/auth_layer.rs b/src/rpc/auth_layer.rs index fcea02885226..c3e7f4ccd94c 100644 --- a/src/rpc/auth_layer.rs +++ b/src/rpc/auth_layer.rs @@ -4,7 +4,7 @@ use crate::auth::{verify_token, JWT_IDENTIFIER}; use crate::key_management::KeyStore; use crate::rpc::{ - auth, beacon, chain, common, eth, gas, miner, mpool, net, node, state, sync, wallet, + auth, beacon, chain, common, eth, gas, miner, mpool, msig, net, node, state, sync, wallet, Permission, RpcMethod as _, CANCEL_METHOD_NAME, }; use ahash::{HashMap, HashMapExt as _}; @@ -43,6 +43,7 @@ static METHOD_NAME2REQUIRED_PERMISSION: Lazy> = Lazy:: sync::for_each_method!(insert); wallet::for_each_method!(insert); eth::for_each_method!(insert); + msig::for_each_method!(insert); access.insert(chain::CHAIN_NOTIFY, Permission::Read); access.insert(CANCEL_METHOD_NAME, Permission::Read); diff --git a/src/rpc/methods/msig.rs b/src/rpc/methods/msig.rs new file mode 100644 index 000000000000..3247592b22d2 --- /dev/null +++ b/src/rpc/methods/msig.rs @@ -0,0 +1,149 @@ +// Copyright 2019-2024 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use crate::rpc::error::ServerError; +use crate::rpc::types::ApiTipsetKey; +use crate::rpc::types::*; +use crate::rpc::{ApiVersion, Ctx, Permission, RpcMethod}; +use crate::shim::actors::multisig::MultisigExt; +use crate::shim::{address::Address, econ::TokenAmount}; +use fil_actor_interface::multisig; +use fvm_ipld_blockstore::Blockstore; +use num_bigint::BigInt; + +macro_rules! for_each_method { + ($callback:ident) => { + $callback!(crate::rpc::msig::MsigGetAvailableBalance); + $callback!(crate::rpc::msig::MsigGetPending); + $callback!(crate::rpc::msig::MsigGetVested); + $callback!(crate::rpc::msig::MsigGetVestingSchedule); + }; +} +pub(crate) use for_each_method; + +pub enum MsigGetAvailableBalance {} + +impl RpcMethod<2> for MsigGetAvailableBalance { + const NAME: &'static str = "Filecoin.MsigGetAvailableBalance"; + const PARAM_NAMES: [&'static str; 2] = ["address", "tipset_key"]; + const API_VERSION: ApiVersion = ApiVersion::V0; + const PERMISSION: Permission = Permission::Read; + + type Params = (Address, ApiTipsetKey); + type Ok = TokenAmount; + + async fn handle( + ctx: Ctx, + (address, ApiTipsetKey(tsk)): Self::Params, + ) -> Result { + let ts = ctx.chain_store.load_required_tipset_or_heaviest(&tsk)?; + let height = ts.epoch(); + let actor = ctx + .state_manager + .get_required_actor(&address, *ts.parent_state())?; + let actor_balance = TokenAmount::from(&actor.balance); + let ms = multisig::State::load(ctx.store(), actor.code, actor.state)?; + let locked_balance = ms.locked_balance(height)?.into(); + let avail_balance = &actor_balance - locked_balance; + Ok(avail_balance) + } +} + +pub enum MsigGetPending {} + +impl RpcMethod<2> for MsigGetPending { + const NAME: &'static str = "Filecoin.MsigGetPending"; + const PARAM_NAMES: [&'static str; 2] = ["address", "tipset_key"]; + const API_VERSION: ApiVersion = ApiVersion::V0; + const PERMISSION: Permission = Permission::Read; + + type Params = (Address, ApiTipsetKey); + type Ok = Vec; + + async fn handle( + ctx: Ctx, + (address, ApiTipsetKey(tsk)): Self::Params, + ) -> Result { + let ts = ctx.chain_store.load_required_tipset_or_heaviest(&tsk)?; + let actor = ctx + .state_manager + .get_required_actor(&address, *ts.parent_state())?; + let ms = multisig::State::load(ctx.store(), actor.code, actor.state)?; + let txns = ms + .get_pending_txn(ctx.store())? + .iter() + .map(|txn| Transaction { + id: txn.id, + to: txn.to.into(), + value: txn.value.clone().into(), + method: txn.method, + params: txn.params.clone(), + approved: txn.approved.iter().map(|item| item.into()).collect(), + }) + .collect(); + Ok(txns) + } +} + +pub enum MsigGetVested {} +impl RpcMethod<3> for MsigGetVested { + const NAME: &'static str = "Filecoin.MsigGetVested"; + const PARAM_NAMES: [&'static str; 3] = ["address", "start_tsk", "end_tsk"]; + const API_VERSION: ApiVersion = ApiVersion::V0; + const PERMISSION: Permission = Permission::Read; + + type Params = (Address, ApiTipsetKey, ApiTipsetKey); + type Ok = BigInt; + + async fn handle( + ctx: Ctx, + (addr, ApiTipsetKey(start_tsk), ApiTipsetKey(end_tsk)): Self::Params, + ) -> Result { + let start_ts = ctx + .chain_store + .load_required_tipset_or_heaviest(&start_tsk)?; + let end_ts = ctx.chain_store.load_required_tipset_or_heaviest(&end_tsk)?; + + match start_ts.epoch().cmp(&end_ts.epoch()) { + std::cmp::Ordering::Greater => Err(ServerError::internal_error( + "start tipset is after end tipset", + None, + )), + std::cmp::Ordering::Equal => Ok(BigInt::from(0)), + std::cmp::Ordering::Less => { + let msig_actor = ctx + .state_manager + .get_required_actor(&addr, *end_ts.parent_state())?; + let ms = multisig::State::load(ctx.store(), msig_actor.code, msig_actor.state)?; + let start_lb: TokenAmount = ms.locked_balance(start_ts.epoch())?.into(); + let end_lb: TokenAmount = ms.locked_balance(end_ts.epoch())?.into(); + Ok(start_lb.atto() - end_lb.atto()) + } + } + } +} + +pub enum MsigGetVestingSchedule {} +impl RpcMethod<2> for MsigGetVestingSchedule { + const NAME: &'static str = "Filecoin.MsigGetVestingSchedule"; + const PARAM_NAMES: [&'static str; 2] = ["address", "tsk"]; + const API_VERSION: ApiVersion = ApiVersion::V0; + const PERMISSION: Permission = Permission::Read; + + type Params = (Address, ApiTipsetKey); + type Ok = MsigVesting; + + async fn handle( + ctx: Ctx, + (addr, ApiTipsetKey(tsk)): Self::Params, + ) -> Result { + let ts = ctx.chain_store.load_required_tipset_or_heaviest(&tsk)?; + + let msig_actor = ctx + .state_manager + .get_required_actor(&addr, *ts.parent_state())?; + let ms = multisig::State::load(ctx.store(), msig_actor.code, msig_actor.state)?; + + Ok(ms.get_vesting_schedule()?) + } +} diff --git a/src/rpc/methods/state.rs b/src/rpc/methods/state.rs index 9677769d3940..61da47205383 100644 --- a/src/rpc/methods/state.rs +++ b/src/rpc/methods/state.rs @@ -34,7 +34,7 @@ use fil_actor_interface::market::DealState; use fil_actor_interface::{ market, miner, miner::{MinerInfo, MinerPower}, - multisig, power, reward, + power, reward, }; use fil_actor_miner_state::v10::{qa_power_for_weight, qa_power_max}; use fil_actors_shared::fvm_ipld_bitfield::BitField; @@ -81,8 +81,6 @@ macro_rules! for_each_method { $callback!(crate::rpc::state::StateGetRandomnessFromBeacon); $callback!(crate::rpc::state::StateReadState); $callback!(crate::rpc::state::StateCirculatingSupply); - $callback!(crate::rpc::state::MsigGetAvailableBalance); - $callback!(crate::rpc::state::MsigGetPending); $callback!(crate::rpc::state::StateVerifiedClientStatus); $callback!(crate::rpc::state::StateVMCirculatingSupplyInternal); $callback!(crate::rpc::state::StateListMiners); @@ -1247,70 +1245,6 @@ impl RpcMethod<1> for StateCirculatingSupply { } } -pub enum MsigGetAvailableBalance {} - -impl RpcMethod<2> for MsigGetAvailableBalance { - const NAME: &'static str = "Filecoin.MsigGetAvailableBalance"; - const PARAM_NAMES: [&'static str; 2] = ["address", "tipset_key"]; - const API_VERSION: ApiVersion = ApiVersion::V0; - const PERMISSION: Permission = Permission::Read; - - type Params = (Address, ApiTipsetKey); - type Ok = TokenAmount; - - async fn handle( - ctx: Ctx, - (address, ApiTipsetKey(tsk)): Self::Params, - ) -> Result { - let ts = ctx.chain_store.load_required_tipset_or_heaviest(&tsk)?; - let height = ts.epoch(); - let actor = ctx - .state_manager - .get_required_actor(&address, *ts.parent_state())?; - let actor_balance = TokenAmount::from(&actor.balance); - let ms = multisig::State::load(ctx.store(), actor.code, actor.state)?; - let locked_balance = ms.locked_balance(height)?.into(); - let avail_balance = &actor_balance - locked_balance; - Ok(avail_balance) - } -} - -pub enum MsigGetPending {} - -impl RpcMethod<2> for MsigGetPending { - const NAME: &'static str = "Filecoin.MsigGetPending"; - const PARAM_NAMES: [&'static str; 2] = ["address", "tipset_key"]; - const API_VERSION: ApiVersion = ApiVersion::V0; - const PERMISSION: Permission = Permission::Read; - - type Params = (Address, ApiTipsetKey); - type Ok = Vec; - - async fn handle( - ctx: Ctx, - (address, ApiTipsetKey(tsk)): Self::Params, - ) -> Result { - let ts = ctx.chain_store.load_required_tipset_or_heaviest(&tsk)?; - let actor = ctx - .state_manager - .get_required_actor(&address, *ts.parent_state())?; - let ms = multisig::State::load(ctx.store(), actor.code, actor.state)?; - let txns = ms - .get_pending_txn(ctx.store())? - .iter() - .map(|txn| Transaction { - id: txn.id, - to: txn.to.into(), - value: txn.value.clone().into(), - method: txn.method, - params: txn.params.clone(), - approved: txn.approved.iter().map(|item| item.into()).collect(), - }) - .collect(); - Ok(txns) - } -} - pub enum StateVerifiedClientStatus {} impl RpcMethod<2> for StateVerifiedClientStatus { diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 375a71dbdf3c..168536dea144 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -55,6 +55,7 @@ pub mod prelude { sync::for_each_method!(export); wallet::for_each_method!(export); eth::for_each_method!(export); + msig::for_each_method!(export); } /// All the methods live in their own folder @@ -109,6 +110,7 @@ mod methods { pub mod gas; pub mod miner; pub mod mpool; + pub mod msig; pub mod net; pub mod node; pub mod state; @@ -276,6 +278,7 @@ where sync::for_each_method!(register); wallet::for_each_method!(register); eth::for_each_method!(register); + msig::for_each_method!(register); module.finish() } diff --git a/src/rpc/types/mod.rs b/src/rpc/types/mod.rs index 1c025fc28a36..df41447efb00 100644 --- a/src/rpc/types/mod.rs +++ b/src/rpc/types/mod.rs @@ -74,6 +74,18 @@ pub struct ApiDealState { lotus_json_with_self!(ApiDealState); +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "PascalCase")] +pub struct MsigVesting { + #[schemars(with = "LotusJson")] + #[serde(with = "crate::lotus_json")] + pub initial_balance: BigInt, + pub start_epoch: ChainEpoch, + pub unlock_duration: ChainEpoch, +} + +lotus_json_with_self!(MsigVesting); + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "PascalCase")] pub struct ApiDealProposal { diff --git a/src/shim/actors/mod.rs b/src/shim/actors/mod.rs index f85ec6cbf9a4..780220f9223e 100644 --- a/src/shim/actors/mod.rs +++ b/src/shim/actors/mod.rs @@ -3,3 +3,4 @@ pub mod market; pub mod miner; +pub mod multisig; diff --git a/src/shim/actors/multisig.rs b/src/shim/actors/multisig.rs new file mode 100644 index 000000000000..7b0387cea605 --- /dev/null +++ b/src/shim/actors/multisig.rs @@ -0,0 +1,11 @@ +// Copyright 2019-2024 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +mod state; + +use crate::rpc::types::MsigVesting; +use fil_actor_interface::multisig::State; + +pub trait MultisigExt { + fn get_vesting_schedule(&self) -> anyhow::Result; +} diff --git a/src/shim/actors/multisig/state.rs b/src/shim/actors/multisig/state.rs new file mode 100644 index 000000000000..92e2dbbb88e5 --- /dev/null +++ b/src/shim/actors/multisig/state.rs @@ -0,0 +1,41 @@ +// Copyright 2019-2024 ChainSafe Systems +// SPDX-License-Identifier: Apache-2.0, MIT + +use super::*; + +impl MultisigExt for State { + fn get_vesting_schedule(&self) -> anyhow::Result { + match self { + State::V8(st) => Ok(MsigVesting { + initial_balance: st.initial_balance.atto().clone(), + start_epoch: st.start_epoch, + unlock_duration: st.unlock_duration, + }), + State::V9(st) => Ok(MsigVesting { + initial_balance: st.initial_balance.atto().clone(), + start_epoch: st.start_epoch, + unlock_duration: st.unlock_duration, + }), + State::V10(st) => Ok(MsigVesting { + initial_balance: st.initial_balance.atto().clone(), + start_epoch: st.start_epoch, + unlock_duration: st.unlock_duration, + }), + State::V11(st) => Ok(MsigVesting { + initial_balance: st.initial_balance.atto().clone(), + start_epoch: st.start_epoch, + unlock_duration: st.unlock_duration, + }), + State::V12(st) => Ok(MsigVesting { + initial_balance: st.initial_balance.atto().clone(), + start_epoch: st.start_epoch, + unlock_duration: st.unlock_duration, + }), + State::V13(st) => Ok(MsigVesting { + initial_balance: st.initial_balance.atto().clone(), + start_epoch: st.start_epoch, + unlock_duration: st.unlock_duration, + }), + } + } +} diff --git a/src/tool/subcommands/api_cmd.rs b/src/tool/subcommands/api_cmd.rs index 6f5462f1702f..7be0bb6f164d 100644 --- a/src/tool/subcommands/api_cmd.rs +++ b/src/tool/subcommands/api_cmd.rs @@ -708,6 +708,15 @@ fn state_tests_with_tipset( Address::new_id(18101), // msig address id tipset.key().into(), ))?), + RpcTest::identity(MsigGetVested::request(( + Address::new_id(18101), // msig address id + tipset.parents().into(), + tipset.key().into(), + ))?), + RpcTest::identity(MsigGetVestingSchedule::request(( + Address::new_id(18101), // msig address id + tipset.key().into(), + ))?), RpcTest::identity(StateGetBeaconEntry::request((tipset.epoch(),))?), // Not easily verifiable by using addresses extracted from blocks as most of those yield `null` // for both Lotus and Forest. Therefore the actor addresses are hardcoded to values that allow