From f39560361d3eb90c30d526b56035caa5ea9d4ee4 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 22 Dec 2023 10:42:24 +0000 Subject: [PATCH 1/5] Show proposal payloads for NNS proposals --- Cargo.lock | 5 + .../canisters/proposals_bot/impl/Cargo.toml | 2 + .../impl/src/governance_clients/common.rs | 41 -- .../impl/src/governance_clients/mod.rs | 2 - .../impl/src/governance_clients/nns.rs | 108 ----- .../impl/src/jobs/retrieve_proposals.rs | 17 +- .../src/jobs/update_finished_proposals.rs | 12 +- .../canisters/proposals_bot/impl/src/lib.rs | 2 +- .../proposals_bot/impl/src/proposals.rs | 22 ++ .../nns_governance/api/Cargo.toml | 3 + .../api/src/queries/list_proposals.rs | 2 + .../nns_governance/api/src/queries/mod.rs | 1 + .../nns_governance/api/src/types.rs | 374 +++++++++++++++++- .../nns_governance/c2c_client/src/lib.rs | 1 + .../sns_governance/api/src/types.rs | 7 +- backend/libraries/types/can.did | 1 + backend/libraries/types/src/proposals.rs | 1 + .../home/proposals/ProposalContent.svelte | 3 +- .../src/services/common/chatMappers.ts | 1 + .../src/services/community/candid/idl.js | 1 + .../src/services/community/candid/types.d.ts | 1 + .../src/services/group/candid/idl.js | 1 + .../src/services/group/candid/types.d.ts | 1 + .../src/services/groupIndex/candid/idl.js | 1 + .../src/services/groupIndex/candid/types.d.ts | 1 + .../src/services/localUserIndex/candid/idl.js | 1 + .../services/localUserIndex/candid/types.d.ts | 1 + .../services/notifications/candid/types.d.ts | 1 + .../src/services/online/candid/types.d.ts | 1 + .../services/proposalsBot/candid/types.d.ts | 1 + .../src/services/registry/candid/types.d.ts | 1 + .../services/storageBucket/candid/types.d.ts | 1 + .../services/storageIndex/candid/types.d.ts | 1 + .../src/services/user/candid/idl.js | 1 + .../src/services/user/candid/types.d.ts | 1 + .../src/services/userIndex/candid/types.d.ts | 1 + .../openchat-shared/src/domain/chat/chat.ts | 2 +- 37 files changed, 454 insertions(+), 170 deletions(-) delete mode 100644 backend/canisters/proposals_bot/impl/src/governance_clients/common.rs delete mode 100644 backend/canisters/proposals_bot/impl/src/governance_clients/mod.rs delete mode 100644 backend/canisters/proposals_bot/impl/src/governance_clients/nns.rs create mode 100644 backend/canisters/proposals_bot/impl/src/proposals.rs create mode 100644 backend/external_canisters/nns_governance/api/src/queries/list_proposals.rs diff --git a/Cargo.lock b/Cargo.lock index 5f6d03ef65..c66f1148c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4087,8 +4087,11 @@ name = "nns_governance_canister" version = "0.1.0" dependencies = [ "candid", + "canister_time", "ic-ledger-types", "serde", + "serde_json", + "types", ] [[package]] @@ -5106,6 +5109,8 @@ dependencies = [ "itertools 0.11.0", "ledger_utils", "msgpack", + "nns_governance_canister", + "nns_governance_canister_c2c_client", "proposals_bot_canister", "rand", "registry_canister", diff --git a/backend/canisters/proposals_bot/impl/Cargo.toml b/backend/canisters/proposals_bot/impl/Cargo.toml index 96c9005c2b..df9a7c112a 100644 --- a/backend/canisters/proposals_bot/impl/Cargo.toml +++ b/backend/canisters/proposals_bot/impl/Cargo.toml @@ -36,6 +36,8 @@ icrc-ledger-types = { workspace = true } itertools = { workspace = true } ledger_utils = { path = "../../../libraries/ledger_utils" } msgpack = { path = "../../../libraries/msgpack" } +nns_governance_canister = { path = "../../../external_canisters/nns_governance/api" } +nns_governance_canister_c2c_client = { path = "../../../external_canisters/nns_governance/c2c_client" } proposals_bot_canister = { path = "../api" } rand = { workspace = true } registry_canister = { path = "../../../canisters/registry/api" } diff --git a/backend/canisters/proposals_bot/impl/src/governance_clients/common.rs b/backend/canisters/proposals_bot/impl/src/governance_clients/common.rs deleted file mode 100644 index 91d55f6de6..0000000000 --- a/backend/canisters/proposals_bot/impl/src/governance_clients/common.rs +++ /dev/null @@ -1,41 +0,0 @@ -use candid::CandidType; -use serde::Deserialize; -use sns_governance_canister::types::ProposalData; -use types::{Proposal, ProposalId, Tally}; - -pub const REWARD_STATUS_ACCEPT_VOTES: i32 = 1; -pub const REWARD_STATUS_READY_TO_SETTLE: i32 = 2; - -pub trait RawProposal: TryInto { - fn id(&self) -> ProposalId; -} - -impl RawProposal for ProposalData { - fn id(&self) -> ProposalId { - self.id.as_ref().map_or(ProposalId::default(), |p| p.id) - } -} - -#[derive(CandidType, Deserialize, Clone)] -pub struct WrappedProposalId { - pub id: ProposalId, -} - -#[derive(CandidType, Deserialize)] -pub struct RawTally { - pub yes: u64, - pub no: u64, - pub total: u64, - pub timestamp_seconds: u64, -} - -impl From for Tally { - fn from(value: RawTally) -> Tally { - Tally { - yes: value.yes, - no: value.no, - total: value.total, - timestamp: value.timestamp_seconds * 1000, - } - } -} diff --git a/backend/canisters/proposals_bot/impl/src/governance_clients/mod.rs b/backend/canisters/proposals_bot/impl/src/governance_clients/mod.rs deleted file mode 100644 index 93b0ac11d2..0000000000 --- a/backend/canisters/proposals_bot/impl/src/governance_clients/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod common; -pub mod nns; diff --git a/backend/canisters/proposals_bot/impl/src/governance_clients/nns.rs b/backend/canisters/proposals_bot/impl/src/governance_clients/nns.rs deleted file mode 100644 index 652f3dc03b..0000000000 --- a/backend/canisters/proposals_bot/impl/src/governance_clients/nns.rs +++ /dev/null @@ -1,108 +0,0 @@ -use self::governance_response_types::{ListProposalInfoResponse, ProposalInfo}; -use super::common::{RawProposal, RawTally, WrappedProposalId}; -use candid::CandidType; -use ic_cdk::api::call::CallResult; -use serde::Deserialize; -use tracing::error; -use types::{CanisterId, NnsNeuronId, ProposalId}; - -pub const TOPIC_NEURON_MANAGEMENT: i32 = 1; -pub const TOPIC_EXCHANGE_RATE: i32 = 2; - -pub async fn list_proposals(governance_canister_id: CanisterId, args: &ListProposalInfo) -> CallResult> { - let method_name = "list_proposals"; - let response: CallResult<(ListProposalInfoResponse,)> = - ic_cdk::api::call::call(governance_canister_id, method_name, (args,)).await; - - if let Err(error) = &response { - error!(method_name, error_code = ?error.0, error_message = error.1.as_str(), "Error calling c2c"); - } - - response.map(|r| r.0.proposal_info) -} - -#[derive(CandidType, Deserialize, Default)] -pub struct ListProposalInfo { - pub limit: u32, - pub before_proposal: Option, - pub exclude_topic: Vec, - pub include_reward_status: Vec, - pub include_status: Vec, -} - -pub mod governance_response_types { - use super::*; - - #[derive(CandidType, Deserialize)] - pub struct ListProposalInfoResponse { - pub proposal_info: Vec, - } - - #[derive(CandidType, Deserialize)] - pub struct ProposalInfo { - pub id: Option, - pub proposer: Option, - pub reject_cost_e8s: u64, - pub proposal: Option, - pub proposal_timestamp_seconds: u64, - pub latest_tally: Option, - pub decided_timestamp_seconds: u64, - pub executed_timestamp_seconds: u64, - pub failed_timestamp_seconds: u64, - pub reward_event_round: u64, - pub topic: i32, - pub status: i32, - pub reward_status: i32, - pub deadline_timestamp_seconds: Option, - } - - impl RawProposal for ProposalInfo { - fn id(&self) -> ProposalId { - self.id.as_ref().map_or(ProposalId::default(), |p| p.id) - } - } - - impl TryFrom for types::Proposal { - type Error = &'static str; - - fn try_from(value: ProposalInfo) -> Result { - types::NnsProposal::try_from(value).map(types::Proposal::NNS) - } - } - - impl TryFrom for types::NnsProposal { - type Error = &'static str; - - fn try_from(p: ProposalInfo) -> Result { - let proposal = p.proposal.ok_or("proposal not set")?; - let now = utils::time::now_millis(); - - Ok(types::NnsProposal { - id: p.id.ok_or("id not set")?.id, - topic: p.topic, - proposer: p.proposer.ok_or("proposer not set")?.id, - created: p.proposal_timestamp_seconds * 1000, - title: proposal.title.ok_or("title not set")?, - summary: proposal.summary, - url: proposal.url, - status: p.status.try_into().unwrap(), - reward_status: p.reward_status.try_into().unwrap(), - tally: p.latest_tally.map(|t| t.into()).unwrap_or_default(), - deadline: p.deadline_timestamp_seconds.ok_or("deadline not set")? * 1000, - last_updated: now, - }) - } - } - - #[derive(CandidType, Deserialize)] - pub struct Proposal { - pub title: Option, - pub summary: String, - pub url: String, - } -} - -#[derive(CandidType, Deserialize, Clone)] -pub struct WrappedNeuronId { - pub id: NnsNeuronId, -} diff --git a/backend/canisters/proposals_bot/impl/src/jobs/retrieve_proposals.rs b/backend/canisters/proposals_bot/impl/src/jobs/retrieve_proposals.rs index d8f603c09d..1f011dd330 100644 --- a/backend/canisters/proposals_bot/impl/src/jobs/retrieve_proposals.rs +++ b/backend/canisters/proposals_bot/impl/src/jobs/retrieve_proposals.rs @@ -1,17 +1,19 @@ -use crate::governance_clients::common::{RawProposal, REWARD_STATUS_ACCEPT_VOTES, REWARD_STATUS_READY_TO_SETTLE}; -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::proposals::{RawProposal, REWARD_STATUS_ACCEPT_VOTES, REWARD_STATUS_READY_TO_SETTLE}; use crate::timer_job_types::{ProcessUserRefundJob, TopUpNeuronJob}; -use crate::{governance_clients, mutate_state, RuntimeState}; +use crate::{mutate_state, RuntimeState}; use canister_timer_jobs::Job; use ic_cdk::api::call::CallResult; +use nns_governance_canister::types::{ListProposalInfo, ProposalInfo}; use sns_governance_canister::types::ProposalData; use std::collections::HashSet; use std::time::Duration; use types::{CanisterId, Milliseconds, Proposal}; use utils::time::MINUTE_IN_MS; +pub const NNS_TOPIC_NEURON_MANAGEMENT: i32 = 1; +pub const NNS_TOPIC_EXCHANGE_RATE: i32 = 2; + const BATCH_SIZE_LIMIT: u32 = 50; const RETRIEVE_PROPOSALS_INTERVAL: Milliseconds = MINUTE_IN_MS; @@ -51,12 +53,15 @@ async fn get_nns_proposals(governance_canister_id: CanisterId) -> CallResult CallResult> { - let response = governance_clients::nns::list_proposals( + let response = nns_governance_canister_c2c_client::list_proposals( governance_canister_id, &ListProposalInfo { limit: 1, - before_proposal: Some(WrappedProposalId { id: proposal_id + 1 }), + before_proposal: Some(nns_governance_canister::types::ProposalId { id: proposal_id + 1 }), ..Default::default() }, ) - .await?; + .await? + .proposal_info; Ok(response.into_iter().next().map(|p| ProposalUpdate { message_id: generate_message_id(governance_canister_id, proposal_id), diff --git a/backend/canisters/proposals_bot/impl/src/lib.rs b/backend/canisters/proposals_bot/impl/src/lib.rs index afde6aab9b..c490562299 100644 --- a/backend/canisters/proposals_bot/impl/src/lib.rs +++ b/backend/canisters/proposals_bot/impl/src/lib.rs @@ -13,12 +13,12 @@ use types::{ }; use utils::env::Environment; -mod governance_clients; mod guards; mod jobs; mod lifecycle; mod memory; mod model; +mod proposals; mod queries; mod timer_job_types; mod updates; diff --git a/backend/canisters/proposals_bot/impl/src/proposals.rs b/backend/canisters/proposals_bot/impl/src/proposals.rs new file mode 100644 index 0000000000..13d507bc7a --- /dev/null +++ b/backend/canisters/proposals_bot/impl/src/proposals.rs @@ -0,0 +1,22 @@ +use nns_governance_canister::types::ProposalInfo; +use sns_governance_canister::types::ProposalData; +use types::{Proposal, ProposalId}; + +pub const REWARD_STATUS_ACCEPT_VOTES: i32 = 1; +pub const REWARD_STATUS_READY_TO_SETTLE: i32 = 2; + +pub trait RawProposal: TryInto { + fn id(&self) -> ProposalId; +} + +impl RawProposal for ProposalData { + fn id(&self) -> ProposalId { + self.id.as_ref().map_or(ProposalId::default(), |p| p.id) + } +} + +impl RawProposal for ProposalInfo { + fn id(&self) -> ProposalId { + self.id.as_ref().map_or(ProposalId::default(), |p| p.id) + } +} diff --git a/backend/external_canisters/nns_governance/api/Cargo.toml b/backend/external_canisters/nns_governance/api/Cargo.toml index fa7c8b3f5f..7a4cece428 100644 --- a/backend/external_canisters/nns_governance/api/Cargo.toml +++ b/backend/external_canisters/nns_governance/api/Cargo.toml @@ -7,5 +7,8 @@ edition = "2021" [dependencies] candid = { workspace = true } +canister_time = { path = "../../../libraries/canister_time" } ic-ledger-types = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } +types = { path = "../../../libraries/types" } diff --git a/backend/external_canisters/nns_governance/api/src/queries/list_proposals.rs b/backend/external_canisters/nns_governance/api/src/queries/list_proposals.rs new file mode 100644 index 0000000000..90292a9fab --- /dev/null +++ b/backend/external_canisters/nns_governance/api/src/queries/list_proposals.rs @@ -0,0 +1,2 @@ +pub type Args = crate::types::ListProposalInfo; +pub type Response = crate::types::ListProposalInfoResponse; diff --git a/backend/external_canisters/nns_governance/api/src/queries/mod.rs b/backend/external_canisters/nns_governance/api/src/queries/mod.rs index 1a199bb81f..fe6ed84e9e 100644 --- a/backend/external_canisters/nns_governance/api/src/queries/mod.rs +++ b/backend/external_canisters/nns_governance/api/src/queries/mod.rs @@ -1 +1,2 @@ pub mod list_neurons; +pub mod list_proposals; diff --git a/backend/external_canisters/nns_governance/api/src/types.rs b/backend/external_canisters/nns_governance/api/src/types.rs index fa9a2a09d9..d2c5fd9a30 100644 --- a/backend/external_canisters/nns_governance/api/src/types.rs +++ b/backend/external_canisters/nns_governance/api/src/types.rs @@ -1,6 +1,6 @@ use candid::{CandidType, Principal}; use ic_ledger_types::AccountIdentifier; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] @@ -307,6 +307,296 @@ pub mod governance_error { } } +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, Default)] +pub struct ListProposalInfo { + pub limit: u32, + pub before_proposal: Option, + pub exclude_topic: Vec, + pub include_reward_status: Vec, + pub include_status: Vec, + pub include_all_manage_neuron_proposals: Option, + pub omit_large_fields: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ListProposalInfoResponse { + pub proposal_info: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ProposalInfo { + pub id: Option, + pub proposer: Option, + pub reject_cost_e8s: u64, + pub proposal: Option, + pub proposal_timestamp_seconds: u64, + pub latest_tally: Option, + pub decided_timestamp_seconds: u64, + pub executed_timestamp_seconds: u64, + pub failed_timestamp_seconds: u64, + pub failure_reason: Option, + pub reward_event_round: u64, + pub topic: i32, + pub status: i32, + pub reward_status: i32, + pub deadline_timestamp_seconds: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Proposal { + pub title: Option, + pub summary: String, + pub url: String, + #[serde(deserialize_with = "ok_or_default")] + pub action: Option, +} + +fn ok_or_default<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + Default, + D: Deserializer<'de>, +{ + Ok(T::deserialize(deserializer).unwrap_or_default()) +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Tally { + pub timestamp_seconds: u64, + pub yes: u64, + pub no: u64, + pub total: u64, +} + +pub mod proposal { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub enum Action { + ManageNeuron(Box), + ManageNetworkEconomics(NetworkEconomics), + Motion(Motion), + ExecuteNnsFunction(ExecuteNnsFunction), + ApproveGenesisKyc(ApproveGenesisKyc), + AddOrRemoveNodeProvider(AddOrRemoveNodeProvider), + RewardNodeProvider(RewardNodeProvider), + SetDefaultFollowees(SetDefaultFollowees), + RewardNodeProviders(RewardNodeProviders), + RegisterKnownNeuron(KnownNeuron), + CreateServiceNervousSystem(CreateServiceNervousSystem), + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct NetworkEconomics { + pub reject_cost_e8s: u64, + pub neuron_minimum_stake_e8s: u64, + pub neuron_management_fee_per_proposal_e8s: u64, + pub minimum_icp_xdr_rate: u64, + pub neuron_spawn_dissolve_delay_seconds: u64, + pub maximum_node_provider_rewards_e8s: u64, + pub transaction_fee_e8s: u64, + pub max_proposals_to_keep_per_topic: u32, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Motion { + pub motion_text: String, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ExecuteNnsFunction { + pub nns_function: i32, + pub payload: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct AddOrRemoveNodeProvider { + pub change: Option, +} + +pub mod add_or_remove_node_provider { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub enum Change { + ToAdd(NodeProvider), + ToRemove(NodeProvider), + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct NodeProvider { + pub id: Option, + pub reward_account: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct RewardNodeProvider { + pub node_provider: Option, + pub amount_e8s: u64, + pub reward_mode: Option, +} + +pub mod reward_node_provider { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct RewardToNeuron { + pub dissolve_delay_seconds: u64, + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct RewardToAccount { + pub to_account: Option, + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub enum RewardMode { + RewardToNeuron(RewardToNeuron), + RewardToAccount(RewardToAccount), + } +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct RewardNodeProviders { + pub rewards: Vec, + pub use_registry_derived_rewards: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct SetDefaultFollowees { + pub default_followees: HashMap, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct KnownNeuron { + pub id: Option, + pub known_neuron_data: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct ApproveGenesisKyc { + pub principals: Vec, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct CreateServiceNervousSystem { + pub name: Option, + pub description: Option, + pub url: Option, + pub logo: Option, + pub fallback_controller_principal_ids: Vec, + pub dapp_canisters: Vec, + pub initial_token_distribution: Option, + pub swap_parameters: Option, + pub ledger_parameters: Option, + pub governance_parameters: Option, +} + +pub mod create_service_nervous_system { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct InitialTokenDistribution { + pub developer_distribution: Option, + pub treasury_distribution: Option, + pub swap_distribution: Option, + } + + pub mod initial_token_distribution { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct DeveloperDistribution { + pub developer_neurons: Vec, + } + + pub mod developer_distribution { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct NeuronDistribution { + pub controller: Option, + pub dissolve_delay: Option, + pub memo: Option, + pub stake: Option, + pub vesting_period: Option, + } + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct TreasuryDistribution { + pub total: Option, + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct SwapDistribution { + pub total: Option, + } + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct SwapParameters { + pub minimum_participants: Option, + pub minimum_icp: Option, + pub maximum_icp: Option, + pub minimum_direct_participation_icp: Option, + pub maximum_direct_participation_icp: Option, + pub minimum_participant_icp: Option, + pub maximum_participant_icp: Option, + pub neuron_basket_construction_parameters: Option, + pub confirmation_text: Option, + pub restricted_countries: Option, + pub start_time: Option, + pub duration: Option, + pub neurons_fund_investment_icp: Option, + pub neurons_fund_participation: Option, + } + + pub mod swap_parameters { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct NeuronBasketConstructionParameters { + pub count: Option, + pub dissolve_delay_interval: Option, + } + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct LedgerParameters { + pub transaction_fee: Option, + pub token_name: Option, + pub token_symbol: Option, + pub token_logo: Option, + } + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct GovernanceParameters { + pub proposal_rejection_fee: Option, + pub proposal_initial_voting_period: Option, + pub proposal_wait_for_quiet_deadline_increase: Option, + pub neuron_minimum_stake: Option, + pub neuron_minimum_dissolve_delay_to_vote: Option, + pub neuron_maximum_dissolve_delay: Option, + pub neuron_maximum_dissolve_delay_bonus: Option, + pub neuron_maximum_age_for_age_bonus: Option, + pub neuron_maximum_age_bonus: Option, + pub voting_reward_parameters: Option, + } + + pub mod governance_parameters { + use super::*; + + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] + pub struct VotingRewardParameters { + pub initial_reward_rate: Option, + pub final_reward_rate: Option, + pub reward_rate_transition_duration: Option, + } + } +} + #[derive(CandidType, Serialize, Deserialize, Clone, Debug)] pub struct NeuronId { pub id: u64, @@ -316,3 +606,85 @@ pub struct NeuronId { pub struct ProposalId { pub id: u64, } + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Tokens { + pub e8s: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Duration { + pub seconds: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Percentage { + pub basis_points: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Canister { + pub id: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Image { + pub base64_encoding: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct GlobalTimeOfDay { + pub seconds_after_utc_midnight: Option, +} + +#[derive(CandidType, Serialize, Deserialize, Clone, Debug)] +pub struct Countries { + pub iso_codes: Vec, +} + +impl TryFrom for types::Proposal { + type Error = String; + + fn try_from(value: ProposalInfo) -> Result { + types::NnsProposal::try_from(value).map(types::Proposal::NNS) + } +} + +impl TryFrom for types::NnsProposal { + type Error = String; + + fn try_from(p: ProposalInfo) -> Result { + let now = canister_time::timestamp_millis(); + let proposal = p.proposal.ok_or("proposal not set".to_string())?; + + Ok(types::NnsProposal { + id: p.id.ok_or("id not set".to_string())?.id, + topic: p.topic, + proposer: p.proposer.ok_or("proposer not set".to_string())?.id.try_into().unwrap(), + created: p.proposal_timestamp_seconds * 1000, + title: proposal.title.ok_or("title not set".to_string())?, + summary: proposal.summary, + url: proposal.url, + status: p.status.try_into().map_err(|s| format!("unknown status: {s}"))?, + reward_status: p + .reward_status + .try_into() + .map_err(|r| format!("unknown reward status: {r}"))?, + tally: p.latest_tally.map(|t| t.into()).unwrap_or_default(), + deadline: p.deadline_timestamp_seconds.ok_or("deadline not set".to_string())?, + payload_text_rendering: proposal.action.and_then(|a| serde_json::to_string_pretty(&a).ok()), + last_updated: now, + }) + } +} + +impl From for types::Tally { + fn from(value: Tally) -> types::Tally { + types::Tally { + yes: value.yes, + no: value.no, + total: value.total, + timestamp: value.timestamp_seconds * 1000, + } + } +} diff --git a/backend/external_canisters/nns_governance/c2c_client/src/lib.rs b/backend/external_canisters/nns_governance/c2c_client/src/lib.rs index c1aab4afbf..782937eaf7 100644 --- a/backend/external_canisters/nns_governance/c2c_client/src/lib.rs +++ b/backend/external_canisters/nns_governance/c2c_client/src/lib.rs @@ -3,6 +3,7 @@ use nns_governance_canister::*; // Queries generate_candid_c2c_call!(list_neurons); +generate_candid_c2c_call!(list_proposals); // Updates generate_candid_c2c_call!(manage_neuron); diff --git a/backend/external_canisters/sns_governance/api/src/types.rs b/backend/external_canisters/sns_governance/api/src/types.rs index 607f0af17d..14c1e0d629 100644 --- a/backend/external_canisters/sns_governance/api/src/types.rs +++ b/backend/external_canisters/sns_governance/api/src/types.rs @@ -2037,10 +2037,13 @@ impl ProposalData { } impl TryFrom for types::Proposal { - type Error = &'static str; + type Error = String; fn try_from(value: ProposalData) -> Result { - types::SnsProposal::try_from(value).map(types::Proposal::SNS) + match types::SnsProposal::try_from(value) { + Ok(p) => Ok(types::Proposal::SNS(p)), + Err(s) => Err(s.to_string()), + } } } diff --git a/backend/libraries/types/can.did b/backend/libraries/types/can.did index 479f2cc5a2..135808d2d2 100644 --- a/backend/libraries/types/can.did +++ b/backend/libraries/types/can.did @@ -948,6 +948,7 @@ type NnsProposal = record { reward_status : ProposalRewardStatus; tally : Tally; deadline : TimestampMillis; + payload_text_rendering : opt text; last_updated : TimestampMillis; }; diff --git a/backend/libraries/types/src/proposals.rs b/backend/libraries/types/src/proposals.rs index 73c5dffa9a..09e17fd2ad 100644 --- a/backend/libraries/types/src/proposals.rs +++ b/backend/libraries/types/src/proposals.rs @@ -94,6 +94,7 @@ pub struct NnsProposal { pub reward_status: ProposalRewardStatus, pub tally: Tally, pub deadline: TimestampMillis, + pub payload_text_rendering: Option, pub last_updated: TimestampMillis, } diff --git a/frontend/app/src/components/home/proposals/ProposalContent.svelte b/frontend/app/src/components/home/proposals/ProposalContent.svelte index 079d208182..1834ab9553 100644 --- a/frontend/app/src/components/home/proposals/ProposalContent.svelte +++ b/frontend/app/src/components/home/proposals/ProposalContent.svelte @@ -74,8 +74,7 @@ $: votingDisabled = voteStatus !== undefined || disable; $: typeValue = getProposalTopicLabel(content, $proposalTopicsStore); $: showFullSummary = proposal.summary.length < 400; - $: payload = - content.proposal.kind === "sns" ? content.proposal.payloadTextRendering : undefined; + $: payload = content.proposal.payloadTextRendering; $: payloadEmpty = payload === undefined || payload === EMPTY_MOTION_PAYLOAD || payload.length === 0; diff --git a/frontend/openchat-agent/src/services/common/chatMappers.ts b/frontend/openchat-agent/src/services/common/chatMappers.ts index 9235b7b33b..b11e208244 100644 --- a/frontend/openchat-agent/src/services/common/chatMappers.ts +++ b/frontend/openchat-agent/src/services/common/chatMappers.ts @@ -454,6 +454,7 @@ function proposal(candid: ApiProposal): Proposal { lastUpdated: Number(p.last_updated), created: Number(p.created), deadline: Number(p.deadline), + payloadTextRendering: optional(p.payload_text_rendering, identity), }; } else if ("SNS" in candid) { const p = candid.SNS; diff --git a/frontend/openchat-agent/src/services/community/candid/idl.js b/frontend/openchat-agent/src/services/community/candid/idl.js index affe098758..7bd8bf5f45 100644 --- a/frontend/openchat-agent/src/services/community/candid/idl.js +++ b/frontend/openchat-agent/src/services/community/candid/idl.js @@ -369,6 +369,7 @@ export const idlFactory = ({ IDL }) => { 'id' : ProposalId, 'url' : IDL.Text, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : IDL.Opt(IDL.Text), 'tally' : Tally, 'title' : IDL.Text, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/community/candid/types.d.ts b/frontend/openchat-agent/src/services/community/candid/types.d.ts index 2f22c1319a..fba5ed48d2 100644 --- a/frontend/openchat-agent/src/services/community/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/community/candid/types.d.ts @@ -1296,6 +1296,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/group/candid/idl.js b/frontend/openchat-agent/src/services/group/candid/idl.js index b43aba1bac..65d5f06352 100644 --- a/frontend/openchat-agent/src/services/group/candid/idl.js +++ b/frontend/openchat-agent/src/services/group/candid/idl.js @@ -318,6 +318,7 @@ export const idlFactory = ({ IDL }) => { 'id' : ProposalId, 'url' : IDL.Text, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : IDL.Opt(IDL.Text), 'tally' : Tally, 'title' : IDL.Text, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/group/candid/types.d.ts b/frontend/openchat-agent/src/services/group/candid/types.d.ts index 9060701e4a..ccc16261a7 100644 --- a/frontend/openchat-agent/src/services/group/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/group/candid/types.d.ts @@ -1141,6 +1141,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/groupIndex/candid/idl.js b/frontend/openchat-agent/src/services/groupIndex/candid/idl.js index 28b4cae340..ec40cfd4a8 100644 --- a/frontend/openchat-agent/src/services/groupIndex/candid/idl.js +++ b/frontend/openchat-agent/src/services/groupIndex/candid/idl.js @@ -312,6 +312,7 @@ export const idlFactory = ({ IDL }) => { 'id' : ProposalId, 'url' : IDL.Text, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : IDL.Opt(IDL.Text), 'tally' : Tally, 'title' : IDL.Text, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts index a73e89615e..db32666e51 100644 --- a/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/groupIndex/candid/types.d.ts @@ -1092,6 +1092,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/localUserIndex/candid/idl.js b/frontend/openchat-agent/src/services/localUserIndex/candid/idl.js index f3a1a96530..20d02b4534 100644 --- a/frontend/openchat-agent/src/services/localUserIndex/candid/idl.js +++ b/frontend/openchat-agent/src/services/localUserIndex/candid/idl.js @@ -183,6 +183,7 @@ export const idlFactory = ({ IDL }) => { 'id' : ProposalId, 'url' : IDL.Text, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : IDL.Opt(IDL.Text), 'tally' : Tally, 'title' : IDL.Text, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts index 132032fff0..d3534a488b 100644 --- a/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/localUserIndex/candid/types.d.ts @@ -1110,6 +1110,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts index 6be1c04843..cf06e35b6a 100644 --- a/frontend/openchat-agent/src/services/notifications/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/notifications/candid/types.d.ts @@ -979,6 +979,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/online/candid/types.d.ts b/frontend/openchat-agent/src/services/online/candid/types.d.ts index 9b27ea30b4..75126965cf 100644 --- a/frontend/openchat-agent/src/services/online/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/online/candid/types.d.ts @@ -989,6 +989,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts index eee6c4510f..596cfbd2ee 100644 --- a/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/proposalsBot/candid/types.d.ts @@ -979,6 +979,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/registry/candid/types.d.ts b/frontend/openchat-agent/src/services/registry/candid/types.d.ts index c3e9cb66a4..b1c152a68a 100644 --- a/frontend/openchat-agent/src/services/registry/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/registry/candid/types.d.ts @@ -995,6 +995,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts index 11fc78d3c2..14a2e7222f 100644 --- a/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageBucket/candid/types.d.ts @@ -1009,6 +1009,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts index cb12c650d5..303d88d99f 100644 --- a/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/storageIndex/candid/types.d.ts @@ -1009,6 +1009,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/user/candid/idl.js b/frontend/openchat-agent/src/services/user/candid/idl.js index fc686dd04f..7ad1537d96 100644 --- a/frontend/openchat-agent/src/services/user/candid/idl.js +++ b/frontend/openchat-agent/src/services/user/candid/idl.js @@ -396,6 +396,7 @@ export const idlFactory = ({ IDL }) => { 'id' : ProposalId, 'url' : IDL.Text, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : IDL.Opt(IDL.Text), 'tally' : Tally, 'title' : IDL.Text, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/user/candid/types.d.ts b/frontend/openchat-agent/src/services/user/candid/types.d.ts index 3334b0c7f6..5e465963b4 100644 --- a/frontend/openchat-agent/src/services/user/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/user/candid/types.d.ts @@ -1289,6 +1289,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts index 493f05725b..0baf255938 100644 --- a/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/userIndex/candid/types.d.ts @@ -1034,6 +1034,7 @@ export interface NnsProposal { 'id' : ProposalId, 'url' : string, 'status' : ProposalDecisionStatus, + 'payload_text_rendering' : [] | [string], 'tally' : Tally, 'title' : string, 'created' : TimestampMillis, diff --git a/frontend/openchat-shared/src/domain/chat/chat.ts b/frontend/openchat-shared/src/domain/chat/chat.ts index 04d92c19b1..0d5de745bd 100644 --- a/frontend/openchat-shared/src/domain/chat/chat.ts +++ b/frontend/openchat-shared/src/domain/chat/chat.ts @@ -294,6 +294,7 @@ export interface ProposalCommon { rewardStatus: ProposalRewardStatus; summary: string; proposer: string; + payloadTextRendering?: string; } export type ManageNeuronResponse = @@ -361,7 +362,6 @@ export enum NnsProposalTopic { export interface SnsProposal extends ProposalCommon { kind: "sns"; action: number; - payloadTextRendering?: string; } export interface ImageContent extends DataContent { From 7edb81d22db275bd18242eac6d6f721cf7df1e32 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 22 Dec 2023 10:52:13 +0000 Subject: [PATCH 2/5] Update CHANGELOGs --- backend/canisters/community/CHANGELOG.md | 4 ++++ backend/canisters/group/CHANGELOG.md | 4 ++++ backend/canisters/proposals_bot/CHANGELOG.md | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/backend/canisters/community/CHANGELOG.md b/backend/canisters/community/CHANGELOG.md index e80deb4b29..c035d81d8a 100644 --- a/backend/canisters/community/CHANGELOG.md +++ b/backend/canisters/community/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Show proposal payloads for NNS proposals ([#5072](https://github.com/open-chat-labs/open-chat/pull/5072)) + ## [[2.0.985](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.985-community)] - 2023-12-19 ### Added diff --git a/backend/canisters/group/CHANGELOG.md b/backend/canisters/group/CHANGELOG.md index d155c15d7a..a63acc50ba 100644 --- a/backend/canisters/group/CHANGELOG.md +++ b/backend/canisters/group/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Show proposal payloads for NNS proposals ([#5072](https://github.com/open-chat-labs/open-chat/pull/5072)) + ## [[2.0.986](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.986-group)] - 2023-12-19 ### Added diff --git a/backend/canisters/proposals_bot/CHANGELOG.md b/backend/canisters/proposals_bot/CHANGELOG.md index 2bef189d41..d60286e0dd 100644 --- a/backend/canisters/proposals_bot/CHANGELOG.md +++ b/backend/canisters/proposals_bot/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added + +- Show proposal payloads for NNS proposals ([#5072](https://github.com/open-chat-labs/open-chat/pull/5072)) + ## [[2.0.960](https://github.com/open-chat-labs/open-chat/releases/tag/v2.0.960-proposals_bot)] - 2023-12-05 ### Changed From 7a84045ee901f51fc4a77de7bc76abc7e5c2db49 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 22 Dec 2023 11:13:39 +0000 Subject: [PATCH 3/5] clippy --- backend/external_canisters/nns_governance/api/src/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/external_canisters/nns_governance/api/src/types.rs b/backend/external_canisters/nns_governance/api/src/types.rs index d2c5fd9a30..c6a13738b7 100644 --- a/backend/external_canisters/nns_governance/api/src/types.rs +++ b/backend/external_canisters/nns_governance/api/src/types.rs @@ -382,7 +382,7 @@ pub mod proposal { SetDefaultFollowees(SetDefaultFollowees), RewardNodeProviders(RewardNodeProviders), RegisterKnownNeuron(KnownNeuron), - CreateServiceNervousSystem(CreateServiceNervousSystem), + CreateServiceNervousSystem(Box), } } @@ -660,7 +660,7 @@ impl TryFrom for types::NnsProposal { Ok(types::NnsProposal { id: p.id.ok_or("id not set".to_string())?.id, topic: p.topic, - proposer: p.proposer.ok_or("proposer not set".to_string())?.id.try_into().unwrap(), + proposer: p.proposer.ok_or("proposer not set".to_string())?.id, created: p.proposal_timestamp_seconds * 1000, title: proposal.title.ok_or("title not set".to_string())?, summary: proposal.summary, From 4bc7c6cbf160b9386d957b8755e8193c3f7c9e1b Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 22 Dec 2023 12:52:56 +0000 Subject: [PATCH 4/5] Return error message if serialization fails --- backend/external_canisters/nns_governance/api/src/types.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/external_canisters/nns_governance/api/src/types.rs b/backend/external_canisters/nns_governance/api/src/types.rs index c6a13738b7..855e81e4ed 100644 --- a/backend/external_canisters/nns_governance/api/src/types.rs +++ b/backend/external_canisters/nns_governance/api/src/types.rs @@ -672,7 +672,9 @@ impl TryFrom for types::NnsProposal { .map_err(|r| format!("unknown reward status: {r}"))?, tally: p.latest_tally.map(|t| t.into()).unwrap_or_default(), deadline: p.deadline_timestamp_seconds.ok_or("deadline not set".to_string())?, - payload_text_rendering: proposal.action.and_then(|a| serde_json::to_string_pretty(&a).ok()), + payload_text_rendering: proposal + .action + .and_then(|a| serde_json::to_string_pretty(&a).ok_or("Failed to serialize payload".to_string())), last_updated: now, }) } From 1f0b77b19cf6db940fd4de82545566b950e7bdf6 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Fri, 22 Dec 2023 13:28:15 +0000 Subject: [PATCH 5/5] Fix --- backend/external_canisters/nns_governance/api/src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/external_canisters/nns_governance/api/src/types.rs b/backend/external_canisters/nns_governance/api/src/types.rs index 855e81e4ed..e9233e12f7 100644 --- a/backend/external_canisters/nns_governance/api/src/types.rs +++ b/backend/external_canisters/nns_governance/api/src/types.rs @@ -674,7 +674,7 @@ impl TryFrom for types::NnsProposal { deadline: p.deadline_timestamp_seconds.ok_or("deadline not set".to_string())?, payload_text_rendering: proposal .action - .and_then(|a| serde_json::to_string_pretty(&a).ok_or("Failed to serialize payload".to_string())), + .map(|a| serde_json::to_string_pretty(&a).unwrap_or("Failed to serialize payload".to_string())), last_updated: now, }) }