Skip to content

Commit

Permalink
Refund deposit if proposal is successful (#4509)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Oct 6, 2023
1 parent 8b0ba03 commit ad2fd61
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 16 deletions.
1 change: 1 addition & 0 deletions backend/canisters/proposals_bot/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Use canister timer rather than heartbeat to retrieve proposals ([#4504](https://github.com/open-chat-labs/open-chat/pull/4504))
- Use canister timer rather than heartbeat to push proposals ([#4506](https://github.com/open-chat-labs/open-chat/pull/4506))
- Use canister timer rather than heartbeat to update proposals ([#4507](https://github.com/open-chat-labs/open-chat/pull/4507))
- Refund deposit if proposal is successful ([#4509](https://github.com/open-chat-labs/open-chat/pull/4509))

## [[2.0.843](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.843-proposals_bot)] - 2023-09-11

Expand Down
49 changes: 37 additions & 12 deletions backend/canisters/proposals_bot/impl/src/jobs/retrieve_proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use crate::governance_clients::common::{RawProposal, REWARD_STATUS_ACCEPT_VOTES,
use crate::governance_clients::nns::governance_response_types::ProposalInfo;
use crate::governance_clients::nns::{ListProposalInfo, TOPIC_EXCHANGE_RATE, TOPIC_NEURON_MANAGEMENT};
use crate::jobs::{push_proposals, update_proposals};
use crate::timer_job_types::{ProcessUserSubmittedProposalAdoptedJob, TimerJob};
use crate::{governance_clients, mutate_state, RuntimeState};
use ic_cdk::api::call::CallResult;
use sns_governance_canister::types::ProposalData;
use std::collections::HashSet;
use std::time::Duration;
use types::{CanisterId, Milliseconds, Proposal};
use types::{CanisterId, Cryptocurrency, Milliseconds, Proposal};
use utils::time::MINUTE_IN_MS;

const BATCH_SIZE_LIMIT: u32 = 50;
Expand Down Expand Up @@ -39,7 +40,7 @@ fn start_next_sync(state: &mut RuntimeState) -> Vec<(CanisterId, bool)> {
async fn get_and_process_nns_proposals(governance_canister_id: CanisterId) {
let response = get_nns_proposals(governance_canister_id).await;

handle_proposals_response(&governance_canister_id, response);
handle_proposals_response(governance_canister_id, response);
}

async fn get_nns_proposals(governance_canister_id: CanisterId) -> CallResult<Vec<ProposalInfo>> {
Expand Down Expand Up @@ -69,7 +70,7 @@ async fn get_nns_proposals(governance_canister_id: CanisterId) -> CallResult<Vec
async fn get_and_process_sns_proposals(governance_canister_id: CanisterId) {
let response = get_sns_proposals(governance_canister_id).await;

handle_proposals_response(&governance_canister_id, response);
handle_proposals_response(governance_canister_id, response);
}

async fn get_sns_proposals(governance_canister_id: CanisterId) -> CallResult<Vec<ProposalData>> {
Expand Down Expand Up @@ -98,13 +99,13 @@ async fn get_sns_proposals(governance_canister_id: CanisterId) -> CallResult<Vec
Ok(proposals)
}

fn handle_proposals_response<R: RawProposal>(governance_canister_id: &CanisterId, response: CallResult<Vec<R>>) {
fn handle_proposals_response<R: RawProposal>(governance_canister_id: CanisterId, response: CallResult<Vec<R>>) {
match response {
Ok(raw_proposals) => {
let proposals: Vec<Proposal> = raw_proposals.into_iter().filter_map(|p| p.try_into().ok()).collect();

mutate_state(|state| {
let previous_active_proposals = state.data.nervous_systems.active_proposals(governance_canister_id);
let previous_active_proposals = state.data.nervous_systems.active_proposals(&governance_canister_id);
let mut no_longer_active: HashSet<_> = previous_active_proposals.into_iter().collect();
for id in proposals.iter().map(|p| p.id()) {
no_longer_active.remove(&id);
Expand All @@ -113,25 +114,49 @@ fn handle_proposals_response<R: RawProposal>(governance_canister_id: &CanisterId
state
.data
.finished_proposals_to_process
.push_back((*governance_canister_id, *id));
.push_back((governance_canister_id, *id));

crate::jobs::update_finished_proposals::start_job_if_required(state);
}

state.data.nervous_systems.process_proposals(
governance_canister_id,
&governance_canister_id,
proposals,
no_longer_active.into_iter().collect(),
);

push_proposals::start_job_if_required(state);
update_proposals::start_job_if_required(state);

let decided_user_submitted_proposals = state
.data
.nervous_systems
.take_newly_decided_user_submitted_proposals(governance_canister_id);

for proposal in decided_user_submitted_proposals {
let now = state.env.now();
let fee = Cryptocurrency::CHAT.fee().unwrap();
if proposal.adopted {
state.data.timer_jobs.enqueue_job(
TimerJob::ProcessUserSubmittedProposalAdopted(ProcessUserSubmittedProposalAdoptedJob {
governance_canister_id,
proposal_id: proposal.proposal_id,
user_id: proposal.user_id,
ledger_canister_id: Cryptocurrency::CHAT.ledger_canister_id().unwrap(),
refund_amount: 4_0000_0000 - fee,
fee,
}),
now,
now,
)
}
}

let now = state.env.now();
state
.data
.nervous_systems
.mark_sync_complete(governance_canister_id, true, now);

push_proposals::start_job_if_required(state);
update_proposals::start_job_if_required(state);
.mark_sync_complete(&governance_canister_id, true, now);
});
}
Err(_) => {
Expand All @@ -140,7 +165,7 @@ fn handle_proposals_response<R: RawProposal>(governance_canister_id: &CanisterId
state
.data
.nervous_systems
.mark_sync_complete(governance_canister_id, false, now);
.mark_sync_complete(&governance_canister_id, false, now);
});
}
}
Expand Down
61 changes: 59 additions & 2 deletions backend/canisters/proposals_bot/impl/src/model/nervous_systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use serde::{Deserialize, Serialize};
use std::cmp::max;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::{BTreeMap, HashMap};
use std::mem;
use types::{
CanisterId, MessageId, MultiUserChat, Proposal, ProposalId, ProposalRewardStatus, ProposalUpdate, SnsNeuronId,
TimestampMillis,
CanisterId, MessageId, MultiUserChat, Proposal, ProposalDecisionStatus, ProposalId, ProposalRewardStatus, ProposalUpdate,
SnsNeuronId, TimestampMillis, UserId,
};

#[derive(Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -134,6 +135,21 @@ impl NervousSystems {
}
}

pub fn take_newly_decided_user_submitted_proposals(
&mut self,
governance_canister_id: CanisterId,
) -> Vec<UserSubmittedProposalResult> {
if let Some(ns) = self
.nervous_systems
.get_mut(&governance_canister_id)
.filter(|ns| !ns.decided_user_submitted_proposals.is_empty())
{
mem::take(&mut ns.decided_user_submitted_proposals)
} else {
Vec::new()
}
}

pub fn queue_proposal_to_update(&mut self, governance_canister_id: CanisterId, proposal: ProposalUpdate) {
if let Some(ns) = self.nervous_systems.get_mut(&governance_canister_id) {
ns.proposals_to_be_updated.pending.insert(proposal.message_id, proposal);
Expand Down Expand Up @@ -199,6 +215,17 @@ impl NervousSystems {
}
}

pub fn record_user_submitted_proposal(
&mut self,
governance_canister_id: CanisterId,
user_id: UserId,
proposal_id: ProposalId,
) {
if let Some(ns) = self.nervous_systems.get_mut(&governance_canister_id) {
ns.active_user_submitted_proposals.insert(proposal_id, user_id);
}
}

pub fn metrics(&self) -> Vec<NervousSystemMetrics> {
self.nervous_systems
.values()
Expand All @@ -223,6 +250,10 @@ pub struct NervousSystem {
neuron_id_for_submitting_proposals: Option<SnsNeuronId>,
#[serde(default)]
sync_in_progress: bool,
#[serde(default)]
active_user_submitted_proposals: HashMap<ProposalId, UserId>,
#[serde(default)]
decided_user_submitted_proposals: Vec<UserSubmittedProposalResult>,
}

#[derive(Serialize, Deserialize, Debug, Default)]
Expand Down Expand Up @@ -251,12 +282,31 @@ impl NervousSystem {
active_proposals: BTreeMap::default(),
neuron_id_for_submitting_proposals: None,
sync_in_progress: false,
active_user_submitted_proposals: HashMap::default(),
decided_user_submitted_proposals: Vec::new(),
}
}

pub fn process_proposal(&mut self, proposal: Proposal) {
let proposal_id = proposal.id();

if let Some(user_id) = self.active_user_submitted_proposals.get(&proposal_id).copied() {
if let Some(adopted) = match proposal.status() {
ProposalDecisionStatus::Unspecified | ProposalDecisionStatus::Open => None,
ProposalDecisionStatus::Adopted | ProposalDecisionStatus::Executed | ProposalDecisionStatus::Failed => {
Some(true)
}
ProposalDecisionStatus::Rejected => Some(false),
} {
self.active_user_submitted_proposals.remove(&proposal_id);
self.decided_user_submitted_proposals.push(UserSubmittedProposalResult {
proposal_id,
user_id,
adopted,
});
}
}

if let Some((previous, message_id)) = self.active_proposals.get_mut(&proposal_id) {
let status = proposal.status();
let reward_status = proposal.reward_status();
Expand Down Expand Up @@ -341,3 +391,10 @@ pub struct ProposalsToUpdate {
pub chat_id: MultiUserChat,
pub proposals: Vec<ProposalUpdate>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct UserSubmittedProposalResult {
pub proposal_id: ProposalId,
pub user_id: UserId,
pub adopted: bool,
}
46 changes: 45 additions & 1 deletion backend/canisters/proposals_bot/impl/src/timer_job_types.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
use crate::mutate_state;
use crate::updates::c2c_submit_proposal::submit_proposal;
use candid::Principal;
use canister_timer_jobs::Job;
use proposals_bot_canister::ProposalToSubmit;
use serde::{Deserialize, Serialize};
use types::{CanisterId, SnsNeuronId, UserId};
use types::icrc1::{Account, TransferArg};
use types::{CanisterId, ProposalId, SnsNeuronId, UserId};
use utils::time::SECOND_IN_MS;

#[derive(Serialize, Deserialize, Clone)]
pub enum TimerJob {
SubmitProposal(SubmitProposalJob),
ProcessUserSubmittedProposalAdopted(ProcessUserSubmittedProposalAdoptedJob),
}

#[derive(Serialize, Deserialize, Clone)]
Expand All @@ -17,10 +22,21 @@ pub struct SubmitProposalJob {
pub proposal: ProposalToSubmit,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ProcessUserSubmittedProposalAdoptedJob {
pub governance_canister_id: CanisterId,
pub proposal_id: ProposalId,
pub user_id: UserId,
pub ledger_canister_id: CanisterId,
pub refund_amount: u128,
pub fee: u128,
}

impl Job for TimerJob {
fn execute(self) {
match self {
TimerJob::SubmitProposal(job) => job.execute(),
TimerJob::ProcessUserSubmittedProposalAdopted(job) => job.execute(),
}
}
}
Expand All @@ -32,3 +48,31 @@ impl Job for SubmitProposalJob {
});
}
}

impl Job for ProcessUserSubmittedProposalAdoptedJob {
fn execute(self) {
let transfer_args = TransferArg {
from_subaccount: None,
to: Account::from(Principal::from(self.user_id)),
fee: Some(self.fee.into()),
created_at_time: None,
memo: None,
amount: self.refund_amount.into(),
};
ic_cdk::spawn(async move {
if icrc1_ledger_canister_c2c_client::icrc1_transfer(self.ledger_canister_id, &transfer_args)
.await
.is_err()
{
mutate_state(|state| {
let now = state.env.now();
state.data.timer_jobs.enqueue_job(
TimerJob::ProcessUserSubmittedProposalAdopted(self),
now + (10 * SECOND_IN_MS),
now,
)
})
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,16 @@ pub(crate) async fn submit_proposal(
Ok(response) => {
if let Some(command) = response.command {
return match command {
manage_neuron_response::Command::MakeProposal(_) => Success,
manage_neuron_response::Command::MakeProposal(p) => {
mutate_state(|state| {
state.data.nervous_systems.record_user_submitted_proposal(
governance_canister_id,
user_id,
p.proposal_id.unwrap().id,
)
});
Success
}
manage_neuron_response::Command::Error(error) => InternalError(format!("{error:?}")),
_ => unreachable!(),
};
Expand Down

0 comments on commit ad2fd61

Please sign in to comment.