Skip to content

Commit

Permalink
Support submitting proposals to any governance canister (#4579)
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Oct 16, 2023
1 parent 6859c12 commit d34d625
Show file tree
Hide file tree
Showing 20 changed files with 162 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ async fn transfer_prize_funds_to_group(

match icrc1::process_transaction(pending_transaction, group).await {
Ok(completed_transaction) => mutate_state(|state| {
let completed_transaction = CompletedCryptoTransaction::from(completed_transaction);
state.data.prizes_sent.push(Prize {
group,
transaction: completed_transaction.clone(),
Expand Down
4 changes: 4 additions & 0 deletions backend/canisters/proposals_bot/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [unreleased]

### Added

- Support submitting proposals to any governance canister ([#4579](https://github.com/open-chat-labs/open-chat/pull/4579))

## [[2.0.884](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.884-proposals_bot)] - 2023-10-12

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
use crate::ProposalToSubmit;
use candid::CandidType;
use serde::{Deserialize, Serialize};
use types::CanisterId;
use types::{icrc1, CanisterId};

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub governance_canister_id: CanisterId,
pub proposal: ProposalToSubmit,
pub transaction: icrc1::CompletedCryptoTransaction,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub enum Response {
Success,
GovernanceCanisterNotSupported,
InsufficientPayment(u128),
Retrying(String),
InternalError(String),
}
30 changes: 28 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 @@ -6,8 +6,8 @@ use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::{BTreeMap, HashMap};
use std::mem;
use types::{
CanisterId, MessageId, Milliseconds, MultiUserChat, Proposal, ProposalDecisionStatus, ProposalId, ProposalRewardStatus,
ProposalUpdate, SnsNeuronId, TimestampMillis, UserId,
icrc1, CanisterId, MessageId, Milliseconds, MultiUserChat, Proposal, ProposalDecisionStatus, ProposalId,
ProposalRewardStatus, ProposalUpdate, SnsNeuronId, TimestampMillis, UserId,
};

#[derive(Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -46,6 +46,26 @@ impl NervousSystems {
.and_then(|ns| ns.neuron_id_for_submitting_proposals)
}

pub fn validate_submit_proposal_payment(
&self,
governance_canister_id: &CanisterId,
payment: icrc1::CompletedCryptoTransaction,
) -> Result<SnsNeuronId, ValidateSubmitProposalPaymentError> {
use ValidateSubmitProposalPaymentError::*;
if let Some(ns) = self.nervous_systems.get(governance_canister_id) {
if let Some(neuron_id) = ns.neuron_id_for_submitting_proposals {
return if payment.ledger != ns.ledger_canister_id {
Err(IncorrectLedger)
} else if u64::try_from(payment.amount).unwrap() < ns.proposal_rejection_fee {
Err(InsufficientPayment(ns.proposal_rejection_fee + ns.transaction_fee))
} else {
Ok(neuron_id)
};
}
}
Err(GovernanceCanisterNotSupported)
}

pub fn set_neuron_id_for_submitting_proposals(
&mut self,
governance_canister_id: &CanisterId,
Expand Down Expand Up @@ -429,3 +449,9 @@ pub struct UserSubmittedProposalResult {
pub user_id: UserId,
pub adopted: bool,
}

pub enum ValidateSubmitProposalPaymentError {
GovernanceCanisterNotSupported,
IncorrectLedger,
InsufficientPayment(u64),
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::model::nervous_systems::ValidateSubmitProposalPaymentError;
use crate::timer_job_types::{LookupUserThenSubmitProposalJob, SubmitProposalJob, TimerJob};
use crate::{mutate_state, read_state, RuntimeState};
use candid::Principal;
Expand All @@ -9,7 +10,7 @@ use sns_governance_canister::types::manage_neuron::Command;
use sns_governance_canister::types::proposal::Action;
use sns_governance_canister::types::{manage_neuron_response, Motion, Proposal, Subaccount, TransferSnsTreasuryFunds};
use tracing::{error, info};
use types::{CanisterId, MultiUserChat, SnsNeuronId, UserDetails, UserId};
use types::{icrc1, CanisterId, MultiUserChat, SnsNeuronId, UserDetails, UserId};
use user_index_canister_c2c_client::{lookup_user, LookupUserError};
use utils::time::SECOND_IN_MS;

Expand All @@ -23,7 +24,7 @@ async fn c2c_submit_proposal(args: Args) -> Response {
user_index_canister_id,
neuron_id,
chat,
} = match read_state(|state| prepare(&args, state)) {
} = match read_state(|state| prepare(args.governance_canister_id, args.transaction, state)) {
Ok(ok) => ok,
Err(response) => return response,
};
Expand All @@ -46,20 +47,25 @@ struct PrepareResult {
chat: MultiUserChat,
}

fn prepare(args: &Args, state: &RuntimeState) -> Result<PrepareResult, Response> {
if let Some(neuron_id) = state
fn prepare(
governance_canister_id: CanisterId,
transaction: icrc1::CompletedCryptoTransaction,
state: &RuntimeState,
) -> Result<PrepareResult, Response> {
use ValidateSubmitProposalPaymentError as E;
match state
.data
.nervous_systems
.get_neuron_id_for_submitting_proposals(&args.governance_canister_id)
.validate_submit_proposal_payment(&governance_canister_id, transaction)
{
Ok(PrepareResult {
Ok(neuron_id) => Ok(PrepareResult {
caller: state.env.caller(),
user_index_canister_id: state.data.user_index_canister_id,
neuron_id,
chat: state.data.nervous_systems.get_chat_id(&args.governance_canister_id).unwrap(),
})
} else {
Err(GovernanceCanisterNotSupported)
chat: state.data.nervous_systems.get_chat_id(&governance_canister_id).unwrap(),
}),
Err(E::GovernanceCanisterNotSupported | E::IncorrectLedger) => Err(GovernanceCanisterNotSupported),
Err(E::InsufficientPayment(min)) => Err(InsufficientPayment(min.into())),
}
}

Expand Down
1 change: 1 addition & 0 deletions backend/canisters/user/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Added

- Support submitting proposals to any governance canister ([#4579](https://github.com/open-chat-labs/open-chat/pull/4579))
- Support sending ICP to ICRC1 accounts ([#4583](https://github.com/open-chat-labs/open-chat/pull/4583))

### Removed
Expand Down
6 changes: 5 additions & 1 deletion backend/canisters/user/api/can.did
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,10 @@ type SavedCryptoAccountsResponse = variant {
type SubmitProposalArgs = record {
governance_canister_id : CanisterId;
proposal : ProposalToSubmit;
ledger : CanisterId;
token : Cryptocurrency;
proposal_rejection_fee : nat;
transaction_fee : nat;
};

type ProposalToSubmit = record {
Expand All @@ -593,7 +597,7 @@ type ProposalToSubmitAction = variant {
type SubmitProposalResponse = variant {
Success;
GovernanceCanisterNotSupported;
Unauthorized;
InsufficientPayment : nat;
UserSuspended;
TransferFailed : text;
Retrying : text;
Expand Down
8 changes: 6 additions & 2 deletions backend/canisters/user/api/src/updates/submit_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ use candid::CandidType;
use proposals_bot_canister::ProposalToSubmit;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use types::CanisterId;
use types::{CanisterId, Cryptocurrency};

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub struct Args {
pub governance_canister_id: CanisterId,
pub proposal: ProposalToSubmit,
pub ledger: CanisterId,
pub token: Cryptocurrency,
pub proposal_rejection_fee: u128,
pub transaction_fee: u128,
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
pub enum Response {
Success,
GovernanceCanisterNotSupported,
Unauthorized,
InsufficientPayment(u128),
UserSuspended,
TransferFailed(String),
Retrying(String),
Expand Down
22 changes: 11 additions & 11 deletions backend/canisters/user/impl/src/updates/submit_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ use canister_tracing_macros::trace;
use ic_cdk_macros::update;
use ledger_utils::icrc1::process_transaction;
use types::icrc1::{Account, PendingCryptoTransaction};
use types::{CanisterId, Cryptocurrency, UserId};
use types::{CanisterId, UserId};
use user_canister::submit_proposal::{Response::*, *};
use utils::consts::{SNS_GOVERNANCE_CANISTER_ID, SNS_LEDGER_CANISTER_ID};

#[update(guard = "caller_is_owner")]
#[trace]
Expand All @@ -23,19 +22,22 @@ async fn submit_proposal(args: Args) -> Response {
};

// Make the crypto transfer
if let Err(failed) = process_transaction(transaction, my_user_id.into()).await {
return TransferFailed(failed.error_message().to_string());
}
let completed_transaction = match process_transaction(transaction, my_user_id.into()).await {
Ok(completed) => completed,
Err(failed) => return TransferFailed(failed.error_message),
};

let c2c_args = proposals_bot_canister::c2c_submit_proposal::Args {
governance_canister_id: args.governance_canister_id,
proposal: args.proposal,
transaction: completed_transaction,
};
match proposals_bot_canister_c2c_client::c2c_submit_proposal(proposals_bot_canister_id, &c2c_args).await {
Ok(proposals_bot_canister::c2c_submit_proposal::Response::Success) => Success,
Ok(proposals_bot_canister::c2c_submit_proposal::Response::GovernanceCanisterNotSupported) => {
GovernanceCanisterNotSupported
}
Ok(proposals_bot_canister::c2c_submit_proposal::Response::InsufficientPayment(min)) => InsufficientPayment(min),
Ok(proposals_bot_canister::c2c_submit_proposal::Response::Retrying(error)) => Retrying(error),
Ok(proposals_bot_canister::c2c_submit_proposal::Response::InternalError(error)) => InternalError(error),
Err(error) => {
Expand All @@ -60,18 +62,16 @@ struct PrepareResult {
fn prepare(args: &Args, state: &RuntimeState) -> Result<PrepareResult, Response> {
if state.data.suspended.value {
Err(UserSuspended)
} else if args.governance_canister_id != SNS_GOVERNANCE_CANISTER_ID {
Err(GovernanceCanisterNotSupported)
} else {
Ok(PrepareResult {
my_user_id: state.env.canister_id().into(),
proposals_bot_canister_id: state.data.proposals_bot_canister_id,
transaction: PendingCryptoTransaction {
ledger: SNS_LEDGER_CANISTER_ID,
token: Cryptocurrency::CHAT,
amount: 4_0000_0000, // 4 CHAT
ledger: args.ledger,
token: args.token.clone(),
amount: args.proposal_rejection_fee + args.transaction_fee,
to: Account::from(state.data.proposals_bot_canister_id),
fee: Cryptocurrency::CHAT.fee().unwrap(),
fee: args.transaction_fee,
memo: None,
created: state.env.now_nanos(),
},
Expand Down
33 changes: 17 additions & 16 deletions backend/libraries/ledger_utils/src/icrc1.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use types::icrc1::{Account, TransferArg};
use types::{CanisterId, CompletedCryptoTransaction, FailedCryptoTransaction};
use types::{
icrc1::{CompletedCryptoTransaction, FailedCryptoTransaction, PendingCryptoTransaction},
CanisterId,
};

pub async fn process_transaction(
transaction: types::icrc1::PendingCryptoTransaction,
transaction: PendingCryptoTransaction,
sender: CanisterId,
) -> Result<CompletedCryptoTransaction, FailedCryptoTransaction> {
let from = Account::from(sender);
Expand All @@ -17,7 +20,7 @@ pub async fn process_transaction(
};

match icrc1_ledger_canister_c2c_client::icrc1_transfer(transaction.ledger, &args).await {
Ok(Ok(block_index)) => Ok(CompletedCryptoTransaction::ICRC1(types::icrc1::CompletedCryptoTransaction {
Ok(Ok(block_index)) => Ok(CompletedCryptoTransaction {
ledger: transaction.ledger,
token: transaction.token.clone(),
amount: transaction.amount,
Expand All @@ -27,7 +30,7 @@ pub async fn process_transaction(
memo: transaction.memo.clone(),
created: transaction.created,
block_index: block_index.0.try_into().unwrap(),
})),
}),
Ok(Err(transfer_error)) => {
let error_message = format!("Transfer failed. {transfer_error:?}");
Err(error_message)
Expand All @@ -37,17 +40,15 @@ pub async fn process_transaction(
Err(error_message)
}
}
.map_err(|error| {
FailedCryptoTransaction::ICRC1(types::icrc1::FailedCryptoTransaction {
ledger: transaction.ledger,
token: transaction.token,
amount: transaction.amount,
fee: transaction.fee,
from: types::icrc1::CryptoAccount::Account(from),
to: types::icrc1::CryptoAccount::Account(transaction.to),
memo: transaction.memo,
created: transaction.created,
error_message: error,
})
.map_err(|error| FailedCryptoTransaction {
ledger: transaction.ledger,
token: transaction.token,
amount: transaction.amount,
fee: transaction.fee,
from: types::icrc1::CryptoAccount::Account(from),
to: types::icrc1::CryptoAccount::Account(transaction.to),
memo: transaction.memo,
created: transaction.created,
error_message: error,
})
}
5 changes: 4 additions & 1 deletion backend/libraries/ledger_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ pub async fn process_transaction(
if t.token == Cryptocurrency::InternetComputer {
nns::process_transaction(t.into(), sender).await
} else {
icrc1::process_transaction(t, sender).await
match icrc1::process_transaction(t, sender).await {
Ok(c) => Ok(c.into()),
Err(f) => Err(f.into()),
}
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions backend/libraries/types/src/cryptocurrency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,18 @@ pub mod icrc1 {
pub created: TimestampNanos,
pub error_message: String,
}

impl From<CompletedCryptoTransaction> for super::CompletedCryptoTransaction {
fn from(value: CompletedCryptoTransaction) -> Self {
super::CompletedCryptoTransaction::ICRC1(value)
}
}

impl From<FailedCryptoTransaction> for super::FailedCryptoTransaction {
fn from(value: FailedCryptoTransaction) -> Self {
super::FailedCryptoTransaction::ICRC1(value)
}
}
}

impl From<icrc1::PendingCryptoTransaction> for nns::PendingCryptoTransaction {
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/src/components/home/MakeProposalModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
const client = getContext<OpenChat>("client");
const dispatch = createEventDispatcher();
const user = client.user;
const proposalCost = BigInt(400000000);
export let selectedMultiUserChat: MultiUserChat;
export let governanceCanisterId: string;
Expand Down Expand Up @@ -63,6 +62,7 @@
$: symbol = tokenDetails.symbol;
$: howToBuyUrl = tokenDetails.howToBuyUrl;
$: transferFee = tokenDetails.transferFee;
$: proposalCost = tokenDetails.nervousSystem?.proposalRejectionFee ?? BigInt(0);
$: requiredFunds = proposalCost + transferFee + transferFee;
$: insufficientFunds = cryptoBalance < requiredFunds;
$: padding = $mobileWidth ? 16 : 24; // yes this is horrible
Expand Down
13 changes: 12 additions & 1 deletion frontend/openchat-agent/src/services/openchatAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2572,7 +2572,18 @@ export class OpenChatAgent extends EventTarget {
submitProposal(
governanceCanisterId: string,
proposal: CandidateProposal,
ledger: string,
token: string,
proposalRejectionFee: bigint,
transactionFee: bigint,
): Promise<SubmitProposalResponse> {
return this.userClient.submitProposal(governanceCanisterId, proposal);
return this.userClient.submitProposal(
governanceCanisterId,
proposal,
ledger,
token,
proposalRejectionFee,
transactionFee,
);
}
}
4 changes: 4 additions & 0 deletions frontend/openchat-agent/src/services/user/candid/idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -1278,8 +1278,12 @@ export const idlFactory = ({ IDL }) => {
'summary' : IDL.Text,
});
const SubmitProposalArgs = IDL.Record({
'token' : Cryptocurrency,
'transaction_fee' : IDL.Nat,
'ledger' : CanisterId,
'governance_canister_id' : CanisterId,
'proposal' : ProposalToSubmit,
'proposal_rejection_fee' : IDL.Nat,
});
const SubmitProposalResponse = IDL.Variant({
'Retrying' : IDL.Text,
Expand Down
Loading

0 comments on commit d34d625

Please sign in to comment.