From 5d3e087dd5169febfb212a68816ddde155a2f00f Mon Sep 17 00:00:00 2001 From: xqxpx Date: Mon, 27 Apr 2026 09:23:34 +0200 Subject: [PATCH 1/4] remove features --- src/backend/env/config.rs | 4 +- src/backend/env/features.rs | 126 ++------------------------------- src/backend/env/memory.rs | 1 + src/backend/env/mod.rs | 8 +-- src/backend/env/post.rs | 32 ++------- src/backend/env/proposals.rs | 21 +----- src/backend/queries.rs | 8 --- src/backend/taggr.did | 2 +- src/backend/updates.rs | 36 ++++------ src/frontend/src/api.ts | 6 +- src/frontend/src/form.tsx | 29 ++------ src/frontend/src/icons.tsx | 15 ---- src/frontend/src/index.tsx | 3 - src/frontend/src/landing.tsx | 4 -- src/frontend/src/post.tsx | 72 +------------------ src/frontend/src/proposals.tsx | 33 --------- src/frontend/src/roadmap.tsx | 73 ------------------- src/frontend/src/settings.tsx | 3 +- src/frontend/src/types.tsx | 9 +-- 19 files changed, 39 insertions(+), 446 deletions(-) delete mode 100644 src/frontend/src/roadmap.tsx diff --git a/src/backend/env/config.rs b/src/backend/env/config.rs index b5e4bac3..3add7484 100644 --- a/src/backend/env/config.rs +++ b/src/backend/env/config.rs @@ -85,7 +85,7 @@ pub struct Config { pub default_max_downvotes: u32, pub domain_cost: Credits, - pub feature_cost: Credits, + pub account_activation_cost: Credits, pub post_cost: Credits, pub blob_cost: Credits, pub poll_cost: Credits, @@ -270,7 +270,7 @@ pub const CONFIG: &Config = &Config { default_max_downvotes: 15, domain_cost: 1000, - feature_cost: 1000, + account_activation_cost: 1000, post_cost: 2, blob_cost: 20, poll_cost: 3, diff --git a/src/backend/env/features.rs b/src/backend/env/features.rs index c7b975ad..f4082bd1 100644 --- a/src/backend/env/features.rs +++ b/src/backend/env/features.rs @@ -1,134 +1,16 @@ use std::collections::HashSet; -use candid::Principal; use serde::{Deserialize, Serialize}; -use crate::{ - env::{post::Meta, Time, YEAR}, - mutate, -}; - -use super::{ - config::CONFIG, - post::{Extension, Post, PostId}, - token::Token, - user::UserId, - State, -}; - -const STATUS_IMPLEMENTED: u8 = 1; -const STATUS_OPEN: u8 = 0; +use super::{user::UserId, Time}; +// Retained only so the `Memory::features` index can still be deserialized. +// The next release drops the field; serde silently ignores it once the migration +// has drained the index. #[derive(Default, Serialize, Deserialize)] pub struct Feature { - // Users who voted for increased priority pub supporters: HashSet, - // 0: requested, 1: implemented pub status: u8, #[serde(default)] pub last_activity: Time, } - -/// Returns a list of all feature ids and current collective voting power of all supporters. -pub fn features<'a>( - state: &'a State, - ids: &'a [PostId], - now: Time, -) -> Box), Token, Feature)> + 'a> { - let transform_feature = move |(post_id, feature): (&PostId, Feature)| { - if feature.status == STATUS_OPEN && feature.last_activity + YEAR <= now { - return None; - } - let tokens = feature - .supporters - .iter() - .map(|user_id| { - state - .users - .get(user_id) - .map(|user| user.total_balance()) - .unwrap_or_default() - }) - .sum::(); - Post::get(state, post_id).map(|post| (post.with_meta(state), tokens, feature)) - }; - - if !ids.is_empty() { - return Box::new( - ids.iter() - .filter_map(move |id| state.memory.features.get(id).map(|feature| (id, feature))) - .filter_map(transform_feature), - ); - } - Box::new(state.memory.features.iter().filter_map(transform_feature)) -} - -pub fn toggle_feature_support(caller: Principal, post_id: PostId, now: Time) -> Result<(), String> { - mutate(|state| { - let user_id = state.principal_to_user(caller).ok_or("no user found")?.id; - let mut feature = state.memory.features.remove(&post_id)?; - if feature.supporters.contains(&user_id) { - feature.supporters.remove(&user_id); - } else { - feature.last_activity = now; - feature.supporters.insert(user_id); - } - state - .memory - .features - .insert(post_id, feature) - .expect("couldn't re-insert feature"); - - Ok(()) - }) -} - -pub fn create_feature(caller: Principal, post_id: PostId, now: Time) -> Result<(), String> { - mutate(|state| { - let user = state.principal_to_user(caller).ok_or("no user found")?; - let user_name = user.name.clone(); - - let post = Post::get(state, &post_id).ok_or("post not found")?; - if post.user != user.id || !matches!(post.extension.as_ref(), Some(&Extension::Feature)) { - return Err("no post with a feature found".into()); - } - - if state.memory.features.get(&post_id).is_some() { - return Err("feature already exists".into()); - } - - state - .memory - .features - .insert( - post_id, - Feature { - supporters: Default::default(), - status: STATUS_OPEN, - last_activity: now, - }, - ) - .expect("couldn't persist feature"); - - let _ = state.system_message( - format!( - "A [new feature](#/post/{}) was created by @{}", - post_id, user_name - ), - CONFIG.dao_realm.into(), - ); - - Ok(()) - }) -} - -pub fn close_feature(state: &mut State, post_id: PostId) -> Result<(), String> { - let mut feature = state.memory.features.remove(&post_id)?; - feature.status = STATUS_IMPLEMENTED; - state - .memory - .features - .insert(post_id, feature) - .expect("couldn't re-insert feature"); - Ok(()) -} diff --git a/src/backend/env/memory.rs b/src/backend/env/memory.rs index 5b4c17b8..5c1dbd83 100644 --- a/src/backend/env/memory.rs +++ b/src/backend/env/memory.rs @@ -22,6 +22,7 @@ pub struct Api { pub struct Memory { api: Api, pub posts: ObjectManager, + // TODO: remove pub features: ObjectManager, #[serde(default)] pub ledger: ObjectManager, diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index d1a2823c..e07ccea9 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -316,7 +316,7 @@ impl State { let (username, len, is_deactivated) = { let user = self.principal_to_user_mut(caller).ok_or("user not found")?; user.change_credits( - CONFIG.feature_cost, + CONFIG.account_activation_cost, CreditsDelta::Minus, "profile privacy change", )?; @@ -2731,12 +2731,6 @@ impl State { Some(Extension::Poll(_)) => { self.pending_polls.remove(&post_id); } - Some(Extension::Feature) => { - self.memory - .features - .remove(&post_id) - .expect("couldn't delete feature"); - } _ => {} }; diff --git a/src/backend/env/post.rs b/src/backend/env/post.rs index b3cad9a5..5d96db5a 100644 --- a/src/backend/env/post.rs +++ b/src/backend/env/post.rs @@ -49,6 +49,8 @@ pub enum Extension { Poll(Poll), Proposal(u32), Repost(PostId), + // Retained so cold-stored posts that reference the old feature extension still + // deserialize. No new posts use it and no behavior is attached to it anymore. Feature, } @@ -352,8 +354,6 @@ impl Post { + blobs as Credits * CONFIG.blob_cost + if matches!(self.extension, Some(Extension::Poll(_))) { CONFIG.poll_cost - } else if matches!(self.extension, Some(Extension::Feature)) { - CONFIG.feature_cost } else { 0 } @@ -771,11 +771,8 @@ impl Post { return; }; - // We never encrypt feature requests and proposals, as they are supposed to be public. - let skip = matches!( - post.extension, - Some(Extension::Feature) | Some(Extension::Proposal(_)) - ); + // We never encrypt proposals, as they are supposed to be public. + let skip = matches!(post.extension, Some(Extension::Proposal(_))); if !skip { let encrypt = !post.encrypted; post.body = xor(&post.body, seed, encrypt); @@ -1326,27 +1323,6 @@ mod tests { assert_eq!(post.body, original_body); assert_eq!(post.patches[0].1, original_patch); - // Test feature post is not encrypted - let feature_id = Post::create( - state, - "Feature request".into(), - &[], - p, - 1, - None, - None, - Some(Extension::Feature), - ) - .unwrap(); - - let feature_body = Post::get(state, &feature_id).unwrap().body.clone(); - assert!(!Post::get(state, &feature_id).unwrap().encrypted); - - Post::crypt(state, feature_id, "test_seed"); - let post = Post::get(state, &feature_id).unwrap(); - assert!(!post.encrypted); - assert_eq!(post.body, feature_body); - // Test proposal post is not encrypted let proposal_id = Post::create( state, diff --git a/src/backend/env/proposals.rs b/src/backend/env/proposals.rs index ce70c108..3eadccab 100644 --- a/src/backend/env/proposals.rs +++ b/src/backend/env/proposals.rs @@ -5,7 +5,7 @@ use super::config::CONFIG; use super::post::{Extension, Post, PostId}; use super::token::{self, account}; use super::user::Predicate; -use super::{features, invoices, RealmId, Time, HOUR, WEEK}; +use super::{invoices, RealmId, Time, HOUR, WEEK}; use super::{user::UserId, State}; use crate::mutate; use crate::token::Token; @@ -38,8 +38,6 @@ pub struct Release { pub hash: String, #[serde(skip)] pub binary: Vec, - #[serde(default)] - pub closed_features: Vec, } type ProposedReward = Token; @@ -213,7 +211,7 @@ impl Proposal { // Proposal accepted. if approvals * 100 >= voting_power * CONFIG.proposal_approval_threshold as u64 { match &mut self.payload { - Payload::Release(release) => { + Payload::Release(_) => { // Invalidate all pending release proposals because they were competing for the // next upgrade. state @@ -225,14 +223,6 @@ impl Proposal { .for_each(|proposal| { proposal.status = Status::Cancelled; }); - - for feature_id in &release.closed_features { - if let Err(err) = features::close_feature(state, *feature_id) { - state - .logger - .error(format!("couldn't close feature: {}", err)); - } - } } Payload::Funding(receiver, tokens) => { mint_tokens(state, *receiver, *tokens, "funding proposal")? @@ -668,7 +658,6 @@ pub mod tests { commit: "sdasd".into(), hash: "".into(), binary: vec![1], - closed_features: vec![], }), time(), ) @@ -720,7 +709,6 @@ pub mod tests { commit: "sdasd".into(), hash: "".into(), binary: vec![1], - closed_features: vec![], }), time(), ) @@ -900,7 +888,6 @@ pub mod tests { commit: "abcdef1234".into(), hash: "0987654321".into(), binary: vec![1, 2, 3, 4], - closed_features: vec![42], }), time(), ) @@ -948,7 +935,6 @@ pub mod tests { commit: "".into(), hash: "".into(), binary: vec![], - closed_features: vec![], }); // Empty commit should fail @@ -962,7 +948,6 @@ pub mod tests { commit: "abcdef".into(), hash: "".into(), binary: vec![], - closed_features: vec![], }); assert_eq!( @@ -975,7 +960,6 @@ pub mod tests { commit: "abcdef".into(), hash: "".into(), binary: vec![1, 2, 3], - closed_features: vec![], }); assert_eq!(release_payload.validate(state), Ok(())); @@ -1175,7 +1159,6 @@ pub mod tests { commit: "".into(), // Empty commit hash: "".into(), binary: vec![], - closed_features: vec![], }); assert_eq!( diff --git a/src/backend/queries.rs b/src/backend/queries.rs index 5fc1d41c..2665bb95 100644 --- a/src/backend/queries.rs +++ b/src/backend/queries.rs @@ -44,14 +44,6 @@ fn auction() { }) } -#[export_name = "canister_query features"] -fn features() { - read(|state| { - let ids: Vec = parse(&arg_data_raw()); - reply(features::features(state, &ids, time()).collect::>()) - }); -} - #[export_name = "canister_query distribution"] fn distribution() { read(|state| { diff --git a/src/backend/taggr.did b/src/backend/taggr.did index d862838b..cee2973a 100644 --- a/src/backend/taggr.did +++ b/src/backend/taggr.did @@ -156,7 +156,7 @@ service : () -> { icrc3_supported_block_types : () -> (vec SupportedBlockType) query; link_cold_wallet : (nat64) -> (Result_1); prod_release : () -> (bool); - propose_release : (nat64, text, vec nat64, blob) -> (Result_4); + propose_release : (nat64, text, blob) -> (Result_4); set_emergency_release : (blob) -> (); stable_mem_read : (nat64) -> (vec record { nat64; blob }) query; unlink_cold_wallet : () -> (Result_1); diff --git a/src/backend/updates.rs b/src/backend/updates.rs index 88dde7cd..64c684b3 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -102,6 +102,18 @@ fn sync_post_upgrade_fixtures() { mutate(|state| { // Fix the stuck hourly routine. state.timers.hourly_pending = false; + + // One-time cleanup: drain the features index. Each remove() frees the + // stable-memory blocks back to the allocator. The next release can then + // drop Memory::features entirely. + let ids: Vec = state.memory.features.iter().map(|(id, _)| *id).collect(); + for id in ids { + if let Err(err) = state.memory.features.remove(&id) { + state + .logger + .error(format!("couldn't drain feature {}: {}", id, err)); + } + } }); } @@ -283,22 +295,6 @@ fn update_wallet_tokens() { reply(User::update_wallet_tokens(read(caller), ids)) } -#[export_name = "canister_update create_feature"] -fn create_feature() { - let post_id: PostId = parse(&arg_data_raw()); - reply(features::create_feature(read(caller), post_id, time())); -} - -#[export_name = "canister_update toggle_feature_support"] -fn toggle_feature_support() { - let post_id: PostId = parse(&arg_data_raw()); - reply(features::toggle_feature_support( - read(caller), - post_id, - time(), - )); -} - #[export_name = "canister_update create_user"] fn create_user() { let (name, invite): (String, String) = parse(&arg_data_raw()); @@ -380,12 +376,7 @@ fn create_proposal() { } #[update] -fn propose_release( - post_id: PostId, - commit: String, - features: Vec, - binary: ByteBuf, -) -> Result { +fn propose_release(post_id: PostId, commit: String, binary: ByteBuf) -> Result { mutate(|state| { proposals::create_proposal( state, @@ -395,7 +386,6 @@ fn propose_release( commit, binary: binary.to_vec(), hash: Default::default(), - closed_features: features, }), time(), ) diff --git a/src/frontend/src/api.ts b/src/frontend/src/api.ts index 97c0c97a..17be6058 100755 --- a/src/frontend/src/api.ts +++ b/src/frontend/src/api.ts @@ -55,7 +55,6 @@ export type Backend = { propose_release: ( postId: PostId, commit: string, - features: PostId[], blob: Uint8Array, ) => Promise; @@ -317,12 +316,11 @@ export const ApiGenerator = ( propose_release: async ( postId: PostId, commit: string, - features: PostId[], blob: Uint8Array, ): Promise => { const arg = IDL.encode( - [IDL.Nat64, IDL.Text, IDL.Vec(IDL.Nat64), IDL.Vec(IDL.Nat8)], - [postId, commit, features, blob], + [IDL.Nat64, IDL.Text, IDL.Vec(IDL.Nat8)], + [postId, commit, blob], ); const response = await call_raw(undefined, "propose_release", arg); if (!response) { diff --git a/src/frontend/src/form.tsx b/src/frontend/src/form.tsx index 57b38e65..e1ba1306 100644 --- a/src/frontend/src/form.tsx +++ b/src/frontend/src/form.tsx @@ -42,14 +42,12 @@ export const Form = ({ urls, content, proposalCreation, - featureRequest, }: { postId?: PostId; comment?: boolean; realmArg?: string; expanded?: boolean; proposalCreation?: boolean; - featureRequest?: boolean; submitCallback: ( value: string, blobs: [string, Uint8Array][], @@ -83,10 +81,7 @@ export const Form = ({ const [poll, setPoll] = React.useState(); const [proposal, setProposal] = React.useState(); const [showTextField, setShowTextField] = React.useState( - !!localStorage.getItem(draftKey) || - expanded || - proposalCreation || - featureRequest, + !!localStorage.getItem(draftKey) || expanded || proposalCreation, ); const [suggestedTags, setSuggestedTags] = React.useState([]); const [suggestedUsers, setSuggestedUsers] = React.useState([]); @@ -154,8 +149,6 @@ export const Form = ({ extension = { Poll: poll }; } else if (repost != undefined) { extension = { Repost: repost }; - } else if (featureRequest) { - extension = "Feature"; } const postId = await submitCallback( value, @@ -172,7 +165,6 @@ export const Form = ({ ? await window.api.propose_release( postId, proposal.Release.commit, - proposal.Release.closed_features, proposal.Release.binary, ) : await window.api.call( @@ -186,17 +178,6 @@ export const Form = ({ `Post could be created, but the proposal creation failed: ${result.Err}`, ); } - } else if (featureRequest) { - let result: any = await window.api.call( - "create_feature", - postId, - ); - if (result && "Err" in result) { - showPopUp( - "warning", - `Post could be created, but the feature request failed: ${result.Err}`, - ); - } } setValue(""); clearTimeout(choresTimer); @@ -340,8 +321,8 @@ export const Form = ({ React.useEffect(() => { clearTimeout(timer); - const { poll_cost, feature_cost } = window.backendCache.config; - const extraCost = poll ? poll_cost : featureRequest ? feature_cost : 0; + const { poll_cost } = window.backendCache.config; + const extraCost = poll ? poll_cost : 0; timer = setTimeout(async () => { setTotalCosts((await costs(value, extraCost)) || totalCosts); }, 1000); @@ -606,8 +587,7 @@ export const Form = ({ /> {isRootPost && !isRepost && - !proposalCreation && - !featureRequest && ( + !proposalCreation && ( 0 && (!realm || user.realms.includes(realm)) && ( { - setFeatures(ev.target.value); - }} - /> -
BINARY{" "} )} - {closed_features.length > 0 && ( -
- CLOSES FEATURES: - - {commaSeparated( - closed_features.map((id) => ( - {id} - )), - )} - -
- )} {!open && (
BUILD HASH: diff --git a/src/frontend/src/roadmap.tsx b/src/frontend/src/roadmap.tsx deleted file mode 100644 index f6e066ef..00000000 --- a/src/frontend/src/roadmap.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from "react"; -import { HeadBar, TabBar } from "./common"; -import { Tokens } from "@dfinity/ledger-icrc/dist/candid/icrc_ledger"; -import { Feature, Meta, Post } from "./types"; -import { newPostCallback } from "./new"; -import { Form } from "./form"; -import { PostFeed } from "./post_feed"; - -export const Roadmap = () => { - const [posts, setPosts] = React.useState<[Post, Meta][]>([]); - const [tab, setTab] = React.useState("OPEN"); - - const loadData = async () => { - const features = await window.api.query< - [[Post, Meta], Tokens, Feature][] - >("features", []); - if (!features) return; - features.sort(([_f1, tokens1], [_f2, tokens2]) => - Number(tokens2 - tokens1), - ); - setPosts( - features - .filter( - ([_post, _tokens, feature]) => - feature.status == (tab == "IMPLEMENTED" ? 1 : 0), - ) - .map(([posts_with_meta]) => posts_with_meta), - ); - }; - - React.useEffect(() => { - loadData(); - }, [tab]); - - return ( - <> - -

New Feature Request Form

-
- - } - /> -
- This is the community driven roadmap for{" "} - {window.backendCache.config.name}. Any user can add a new - feature requests for{" "} - {window.backendCache.config.feature_cost} credits. - All features are sorted by the voting power of the supporters. - The order of features signalizes their priority as defined by - DAO's support. When creating a new feature request, be clear and - concise, link all previous discussions and design documents. -
- - posts} - /> - - ); -}; diff --git a/src/frontend/src/settings.tsx b/src/frontend/src/settings.tsx index 62e779bc..8b1b62f7 100644 --- a/src/frontend/src/settings.tsx +++ b/src/frontend/src/settings.tsx @@ -430,7 +430,8 @@ export const Settings = ({ invite }: { invite?: string }) => { to activate your account again, make sure you can recover this password later. An account activation/deactivation costs{" "} - {window.backendCache.config.feature_cost} credits. + {window.backendCache.config.account_activation_cost}{" "} + credits.

{onCanonicalDomain() ? ( <> diff --git a/src/frontend/src/types.tsx b/src/frontend/src/types.tsx index d11faff3..eb0ec40a 100644 --- a/src/frontend/src/types.tsx +++ b/src/frontend/src/types.tsx @@ -67,11 +67,6 @@ export type Summary = { export type Mode = "Mining" | "Rewards" | "Credits"; -export type Feature = { - supporters: UserId[]; - status: number; -}; - export type Extension = | { ["Poll"]: Poll; @@ -82,6 +77,7 @@ export type Extension = | { ["Proposal"]: number; } + // Retained so cold-stored posts referencing the old feature extension still parse. | "Feature"; export type Rewards = { @@ -93,7 +89,6 @@ export type Release = { commit: string; hash: string; binary: Uint8Array; - closed_features: PostId[]; }; export type Icrc1Canister = { @@ -373,7 +368,7 @@ export type Config = { max_credits_mint_kilos: number; logo: string; poll_revote_deadline_hours: number; - feature_cost: number; + account_activation_cost: number; blob_cost: number; poll_cost: number; max_post_length: number; From 0fbfbd057172c90a47b7e272bd2011c638b702f7 Mon Sep 17 00:00:00 2001 From: xqxpx Date: Mon, 27 Apr 2026 09:55:08 +0200 Subject: [PATCH 2/4] remove external tips --- src/backend/env/canisters.rs | 12 -- src/backend/env/mod.rs | 6 - src/backend/env/post.rs | 5 - src/backend/env/realms.rs | 10 - src/backend/env/tip.rs | 342 ------------------------------ src/backend/updates.rs | 9 - src/frontend/src/post.tsx | 147 ++----------- src/frontend/src/realms.tsx | 120 +---------- src/frontend/src/tipping.tsx | 156 ++------------ src/frontend/src/token_select.tsx | 90 -------- src/frontend/src/types.tsx | 9 - 11 files changed, 37 insertions(+), 869 deletions(-) delete mode 100644 src/backend/env/tip.rs delete mode 100755 src/frontend/src/token_select.tsx diff --git a/src/backend/env/canisters.rs b/src/backend/env/canisters.rs index 068dfefb..e067063c 100644 --- a/src/backend/env/canisters.rs +++ b/src/backend/env/canisters.rs @@ -16,7 +16,6 @@ use ic_cdk_management_canister::{ }; use ic_ledger_types::{Tokens, MAINNET_GOVERNANCE_CANISTER_ID}; use ic_xrc_types::{Asset, GetExchangeRateRequest, GetExchangeRateResult}; -use icrc_ledger_types::icrc3::transactions::{GetTransactionsRequest, GetTransactionsResponse}; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::collections::HashMap; @@ -366,14 +365,3 @@ pub async fn top_up() { } } } - -pub async fn get_transactions( - canister_id: Principal, - args: GetTransactionsRequest, -) -> Result { - let (response,): (GetTransactionsResponse,) = - call_canister(canister_id, "get_transactions", (args,)) - .await - .map_err(|e| format!("failed to call ledger: {:?}", e))?; - Ok(response) -} diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index e07ccea9..e1a7bdb8 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -51,7 +51,6 @@ pub mod realms; pub mod reports; pub mod search; pub mod storage; -pub mod tip; pub mod token; pub mod user; @@ -746,7 +745,6 @@ impl State { cleanup_penalty, adult_content, comments_filtering, - tokens, max_downvotes, .. } = realm; @@ -763,9 +761,6 @@ impl State { if !logo.is_empty() { realm.logo = logo; } - if tokens.as_ref().is_some_and(|t| t.len() > 50) { - return Err("tokens count above 50".into()); - } let description_change = realm.description != description; realm.description = description; if realm.controllers != controllers { @@ -796,7 +791,6 @@ impl State { realm.last_setting_update = time(); realm.adult_content = adult_content; realm.comments_filtering = comments_filtering; - realm.tokens = tokens; if description_change { self.notify_with_filter( &|user| user.realms.contains(&realm_id), diff --git a/src/backend/env/post.rs b/src/backend/env/post.rs index 5d96db5a..baf0de9f 100644 --- a/src/backend/env/post.rs +++ b/src/backend/env/post.rs @@ -3,7 +3,6 @@ use std::cmp::{Ordering, PartialOrd}; use super::config::DOWNVOTE_REACTION_ID; use super::*; use super::{storage::Storage, user::UserId}; -use crate::env::tip::Tip; use crate::mutate; use ic_cdk::api::msg_caller as caller; use serde::{Deserialize, Serialize}; @@ -96,9 +95,6 @@ pub struct Post { #[serde(default)] pub encrypted: bool, - #[serde(default)] - pub external_tips: Vec, - #[serde(default)] pub hidden_for: Vec, } @@ -163,7 +159,6 @@ impl Post { encrypted: false, realm, heat, - external_tips: Default::default(), hidden_for: Default::default(), } } diff --git a/src/backend/env/realms.rs b/src/backend/env/realms.rs index 53add6c3..4c4f4603 100644 --- a/src/backend/env/realms.rs +++ b/src/backend/env/realms.rs @@ -60,8 +60,6 @@ pub struct Realm { pub posts: Vec, pub adult_content: bool, pub comments_filtering: bool, - /// Tokens allowed to appear in realm like tips - pub tokens: Option>, } impl Realm { @@ -498,21 +496,13 @@ pub(crate) mod tests { Err("not authorized".to_string()) ); - let mut tokens = BTreeSet::new(); - tokens.insert(pr(99)); let realm = Realm { controllers, description: "New test description".into(), - tokens: Some(tokens.clone()), ..Default::default() }; assert_eq!(state.edit_realm(p0, name.clone(), realm), Ok(())); - assert_eq!( - state.realms.get(&name).unwrap().tokens, - Some(tokens.clone()), - ); - assert_eq!( state.realms.get(&name).unwrap().description, new_description diff --git a/src/backend/env/tip.rs b/src/backend/env/tip.rs deleted file mode 100644 index 07a8ed63..00000000 --- a/src/backend/env/tip.rs +++ /dev/null @@ -1,342 +0,0 @@ -use candid::Nat; -use icrc_ledger_types::icrc3::transactions::GetTransactionsRequest; - -use super::{token::Memo, *}; - -#[derive(Clone, Serialize, Deserialize)] -pub struct Tip { - sender_id: UserId, - - canister_id: Principal, - amount: u128, - /// Index from external transaction - index: u64, -} - -#[allow(clippy::too_many_arguments)] -fn create_post_tip( - state: &mut State, - post_id: PostId, - canister_id: Principal, - amount: u128, - memo: Memo, - to: Principal, - from: Principal, - index: u64, -) -> Result { - let receiver_id = state.principal_to_user(to).ok_or("receiver not found")?.id; - let post = Post::get(state, &post_id).ok_or("post not found")?; - if post.user != receiver_id { - return Err("receiver does not match with post creator".to_string()); - } - - let (sender_id, sender_name) = state - .principal_to_user(from) - .map(|sender| (sender.id, sender.name.clone())) - .ok_or("sender not found")?; - - let memo = memo_to_u64(memo)?; - - if memo != post_id { - return Err(format!( - "memo {} does not match with post id {}", - memo, post_id - )); - } - - if post.external_tips.iter().any(|tip| tip.index == index) { - return Err("tip external index already exists".to_string()); - } - - let tip = Tip { - sender_id, - canister_id, - amount, - index, - }; - - Post::mutate(state, &post_id, |post| { - post.external_tips.push(tip.clone()); - Ok(()) - }) - .map_err(|err| format!("failed to add tip to post: {}", err))?; - - state - .users - .get_mut(&receiver_id) - .expect("user not found") - .notify_about_post(format!("You got a tip from @{}", sender_name), post_id); - - Ok(tip) -} - -fn memo_to_u64(memo: Vec) -> Result { - let mut padded = [0u8; 8]; - let len = std::cmp::min(memo.len(), 8); - padded[8 - len..].copy_from_slice(&memo[..len]); - Ok(u64::from_be_bytes(padded)) -} - -pub async fn add_tip( - post_id: PostId, - canister_id: String, - caller: Principal, - start_index: u64, -) -> Result { - mutate(|state| match state.principal_to_user(caller) { - Some(user) => { - // DoS protection - state.charge(user.id, CONFIG.tipping_cost, "tipping".to_string())?; - Ok::<(), String>(()) - } - _ => Err("user not found".into()), - })?; - - let canister_id = - Principal::from_text(&canister_id).map_err(|_| "invalid canister id".to_string())?; - match try_tip(post_id, canister_id, caller, start_index).await { - Ok(tip) => Ok(tip), - Err(e) => { - // Penalize user for failed tip attempt since inter-canister calls are expensive - mutate(|state| { - let sender_id = state.principal_to_user(caller).expect("user not found").id; - state.charge( - sender_id, - CONFIG.tipping_cost * 2, - format!("failed external tipping for post {}", post_id), - ) - })?; - - Err(e) - } - } -} - -async fn try_tip( - post_id: PostId, - canister_id: Principal, - caller: Principal, - start_index: u64, -) -> Result { - let args = GetTransactionsRequest { - start: Nat::from(start_index), - length: Nat::from(1_u128), - }; - let response = canisters::get_transactions(canister_id, args).await?; - let Some(transfer) = response - .transactions - .first() - .and_then(|tx| tx.transfer.as_ref()) - else { - return Err(format!("no transfer transaction at index {}", start_index)); - }; - - mutate(|state| { - let amount = u128::try_from(&transfer.amount.0).expect("Wrong amount"); - let memo = transfer.memo.as_ref().unwrap().0.to_vec(); - if transfer.from.owner != caller { - return Err("caller is not transaction sender".into()); - } - create_post_tip( - state, - post_id, - canister_id, - amount, - memo, - transfer.to.owner, - transfer.from.owner, - start_index, - ) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::env::{ - realms::create_realm, - tests::{create_user, pr}, - }; - - /// ### Returns - /// post_id, post_owner, receiver, realm_id - fn setup() -> (u64, Principal, Principal, RealmId) { - // Create users, realm and post - mutate(|state| { - let p = pr(0); - let p2 = pr(1); - let user_id = create_user(state, p); - create_user(state, p2); - - state - .principal_to_user_mut(p) - .unwrap() - .change_credits(100, CreditsDelta::Plus, "") - .unwrap(); - - let realm_name = "test-realm".to_string(); - create_realm( - state, - p, - realm_name.clone(), - Realm { - controllers: vec![user_id].into_iter().collect(), - ..Default::default() - }, - ) - .expect("realm creation failed"); - - state.toggle_realm_membership(p, realm_name.clone()); - - ( - Post::create( - state, - "Hello world!".into(), - &[], - p, - 0, - None, - Some(realm_name.clone()), - None, - ) - .unwrap(), - p, - p2, - realm_name.clone(), - ) - }) - } - - #[test] - fn test_tip_validity() { - // Create users, realm and post - let (post_id, principal, principal_2, realm_id) = setup(); - let canister_id = pr(100); - - // Success - mutate(|state| { - // Add tokens - let tokens = BTreeSet::from([canister_id]); - state - .realms - .get_mut(&realm_id) - .expect("realm not found") - .tokens = Some(tokens); - - // Create tip - let r = create_post_tip( - state, - post_id, - canister_id, - 1, - vec![0, 0, 0, 0], - principal, - principal_2, - 0, - ); - assert_eq!(r.map(|tip| tip.index), Ok(0)); - }); - - // Post not found - mutate(|state| { - let r = create_post_tip( - state, - 2, // Uknown post - canister_id, - 1, - vec![0, 0, 0, 0], - principal, - principal_2, - 0, - ); - assert_eq!(r.err(), Some("post not found".to_string())); - }); - - // Receiver not found - mutate(|state| { - let r = create_post_tip( - state, - post_id, - canister_id, - 1, - vec![0, 0, 0, 0], - pr(3), // Uknown receiver - principal_2, - 0, - ); - assert_eq!(r.err(), Some("receiver not found".to_string())); - }); - - // Sender not found - mutate(|state| { - let r = create_post_tip( - state, - post_id, - canister_id, - 1, - vec![0, 0, 0, 0], - principal, - pr(4), // Uknown sender - 0, - ); - assert_eq!(r.err(), Some("sender not found".to_string())); - }); - - // Receiver does not match with post creator - mutate(|state| { - let r = create_post_tip( - state, - post_id, - canister_id, - 1, - vec![0, 0, 0, 0], - principal_2, // Post creator is principal - principal_2, - 0, - ); - assert_eq!( - r.err(), - Some("receiver does not match with post creator".to_string()) - ); - }); - - // Memo does not match with post id - mutate(|state| { - let r = create_post_tip( - state, - post_id, - canister_id, - 1, - vec![0, 0, 0, 1], // Different memo - principal, - principal_2, - 0, - ); - assert_eq!( - r.err(), - Some(format!( - "memo {} does not match with post id {}", - 1, post_id - )) - ); - }); - - // Tip external index already exists - mutate(|state| { - let r = create_post_tip( - state, - post_id, - canister_id, - 1, - vec![0, 0, 0, 0], - principal, - principal_2, - 0, // External index 0 already exists - ); - assert_eq!( - r.err(), - Some("tip external index already exists".to_string()) - ); - }); - } -} diff --git a/src/backend/updates.rs b/src/backend/updates.rs index 64c684b3..3e546c7b 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -2,7 +2,6 @@ use crate::env::{ domains::{change_domain_config, DomainConfig}, proposals::{Payload, Release}, realms::{clean_up_realm, Realm, RealmId}, - tip::add_tip, user::{Mode, UserFilter}, }; @@ -714,14 +713,6 @@ fn cancel_bid() { in_executor_context(|| spawn(async { reply(auction::cancel_bid(read(caller)).await) })); } -#[export_name = "canister_update add_external_icrc_transaction"] -fn add_external_icrc_transaction() { - let (canister_id, start_index, post_id): (String, u64, PostId) = parse(&arg_data_raw()); - in_executor_context(|| { - spawn(async move { reply(add_tip(post_id, canister_id, read(caller), start_index).await) }) - }); -} - #[update] fn backup() { mutate(|state| state.create_backup()) diff --git a/src/frontend/src/post.tsx b/src/frontend/src/post.tsx index 3fbadf11..7efbe5d8 100644 --- a/src/frontend/src/post.tsx +++ b/src/frontend/src/post.tsx @@ -27,9 +27,6 @@ import { postAllowed, NotAllowed, onCanonicalDomain, - getCanistersMetaData, - shortenTokensAmount, - icpSwapLogoFallback, popUp, domain, } from "./common"; @@ -54,14 +51,13 @@ import { } from "./icons"; import { ProposalView } from "./proposals"; import { DEFAULT_REACTION_HOLD_TIME } from "./settings"; -import { Icrc1Canister, Post, PostId, PostTip, Realm, UserId } from "./types"; +import { Post, PostId, Realm, UserId } from "./types"; import { UserLink, UserList, populateUserNameCache, validUserId, } from "./user_resolve"; -import { CANISTER_ID } from "./env"; import { TippingPopup } from "./tipping"; export const PostView = ({ @@ -514,72 +510,27 @@ const PostInfo = ({ const [realmData, setRealmData] = React.useState(); const [loaded, setLoaded] = React.useState(false); const [loading, setLoading] = React.useState(false); - const [externalTips, setExternalTips] = React.useState([]); - const [canistersMetaData, setCanisterMetaData] = React.useState< - Record - >({}); // All tokens data - const [allowedTippingCanisterIds, setAllowedTippingCanisterIds] = - React.useState([]); // Allowed tipping tokens const loadData = async () => { - // Load realm data asynchronously - const realmPromise = post.realm - ? window.api - .query("realms", [post.realm]) - .then((realmData) => { - setRealmData(realmData?.at(0)); - return realmData?.at(0); - }) - : Promise.resolve(undefined); + if (post.realm) { + window.api + .query("realms", [post.realm]) + .then((realmData) => setRealmData(realmData?.at(0))) + .catch(console.error); + } const ids: UserId[] = [post.user] // @ts-ignore .concat(...Object.values(reactions)) // @ts-ignore .concat(post.watchers) // @ts-ignore - .concat(Object.keys(post.tips).map(Number)) - // External tip senders - .concat( - post.external_tips?.map(({ sender_id }) => sender_id) || [], - ); + .concat(Object.keys(post.tips).map(Number)); await populateUserNameCache(ids, setLoading); - realmPromise - .then((realm) => loadExternalTipsData(realm)) - .catch(console.error); - setLoaded(true); }; - /** Load canister data of external tips */ - const loadExternalTipsData = async (realm: Realm | undefined) => { - const externalTips = post.external_tips || []; - const allTokenIds = [ - CANISTER_ID, - ...externalTips.map(({ canister_id }) => canister_id), - ]; - const allowedTippingCanisterIds = [CANISTER_ID]; - if (realm?.tokens) { - allowedTippingCanisterIds.push(...realm.tokens); - allTokenIds.push(...realm.tokens); - } - - const metadata = await getCanistersMetaData([ - ...new Set(allTokenIds), - ]).catch(() => new Map()); - - setCanisterMetaData(Object.fromEntries(metadata)); - - setAllowedTippingCanisterIds( - [...new Set(allowedTippingCanisterIds)].filter( - (canisterId) => !!metadata.get(canisterId), - ), - ); - - setExternalTips(externalTips); - }; - const initialRef = React.useRef(false); React.useEffect(() => { if (!initialRef.current) { @@ -765,31 +716,19 @@ const PostInfo = ({ {!postAuthor && onCanonicalDomain() && validUserId(post.user) && ( - <> - } - classNameArg="max_width_col" - onClick={async () => - popUp( - , - ) - } - /> - + } + classNameArg="max_width_col" + onClick={async () => + popUp( + , + ) + } + /> )} {realmController && isRoot(post) && ( )} - {externalTips.length > 0 && - Object.keys(canistersMetaData).length > 0 && ( -
- EXTERNAL TIPS:{" "} - {commaSeparated( - externalTips.map((tip) => ( - - - - {shortenTokensAmount( - tip.amount, - canistersMetaData[ - tip.canister_id - ]?.decimals || 0, - )}{" "} - {canistersMetaData[tip.canister_id] - ?.symbol || ""}{" "} - - from{" "} - { - - } - - )), - )} -
- )} {Object.keys(reactions).length > 0 && (
{Object.entries(reactions).map(([reactId, users]) => ( @@ -1120,8 +1018,7 @@ const PostBar = ({ )} {!showEmojis && ( <> - {(post.tips.length > 0 || - !!post.external_tips?.length) && ( + {post.tips.length > 0 && ( )} {post.reposts.length > 0 && ( diff --git a/src/frontend/src/realms.tsx b/src/frontend/src/realms.tsx index 2deae1bc..3bdba0e5 100644 --- a/src/frontend/src/realms.tsx +++ b/src/frontend/src/realms.tsx @@ -1,4 +1,3 @@ -import { Principal } from "@dfinity/principal"; import * as React from "react"; import { loadFile } from "./form"; import { @@ -13,22 +12,17 @@ import { foregroundColor, showPopUp, domain, - getCanistersMetaData, - getUserTokens, - icpSwapLogoFallback, } from "./common"; import { Content } from "./content"; import { Close } from "./icons"; import { getTheme, setRealmUI } from "./theme"; -import { Icrc1Canister, Realm, Theme, UserFilter } from "./types"; +import { Realm, Theme, UserFilter } from "./types"; import { USER_CACHE, UserList, populateUserNameCache, userNameToIds, } from "./user_resolve"; -import { TokenSelect } from "./token_select"; -import { CANISTER_ID } from "./env"; let timer: any = null; @@ -62,13 +56,9 @@ export const RealmForm = ({ existingName }: { existingName?: string }) => { posts: [], adult_content: false, comments_filtering: true, - tokens: undefined, }); const [controllersString, setControllersString] = React.useState(""); const [whitelistString, setWhitelistString] = React.useState(""); - const [canistersMetaData, setCanisterMetaData] = React.useState< - Record - >({}); const loadRealm = async () => { let result = @@ -81,20 +71,6 @@ export const RealmForm = ({ existingName }: { existingName?: string }) => { return realm; }; - const loadTokens = async (realm?: Realm) => { - const canisterIds = new Set([CANISTER_ID]); - - realm?.tokens?.forEach((id) => canisterIds.add(id)); - - await getUserTokens(window?.user).then(async (tokens) => { - tokens.forEach(({ canisterId }) => canisterIds.add(canisterId)); - window.user?.wallet_tokens?.forEach((id) => canisterIds.add(id)); - }); - - const map = await getCanistersMetaData([...canisterIds]); - setCanisterMetaData(Object.fromEntries(map)); - }; - const setStrings = async (realm: Realm) => { await populateUserNameCache(realm.whitelist.concat(realm.controllers)); setWhitelistString( @@ -106,29 +82,9 @@ export const RealmForm = ({ existingName }: { existingName?: string }) => { }; React.useEffect(() => { - if (editing) loadRealm().then((r) => loadTokens(r)); - else loadTokens(); + if (editing) loadRealm(); }, []); - const realmTokenInfo = (token: string): JSX.Element => { - const metadata = canistersMetaData[token]; - if (!metadata) return <>; - return ( - - - {metadata.symbol} - - ); - }; - const { logo, description, @@ -465,78 +421,6 @@ export const RealmForm = ({ existingName }: { existingName?: string }) => {

-

Tokens enabled for tipping

-
- {Object.keys(canistersMetaData).length > 0 && ( -
- [ - canisterId, - canistersMetaData[canisterId], - ], - )} - onSelectionChange={(canisterId) => { - if (realm.tokens?.includes(canisterId)) { - return; - } - realm.tokens = [ - ...(realm.tokens || []), - canisterId, - ]; - setRealm({ ...realm }); - }} - /> - { - const canisterIds = - e.target.value - ?.split(",") - .filter(Boolean) || []; - try { - canisterIds.forEach( - (canisterId) => - canisterId && - Principal.fromText(canisterId), - ); // Try catch - const metadata = - await getCanistersMetaData( - canisterIds, - ); - if (!metadata) { - return alert( - "Could not find canister metadata", - ); - } - realm.tokens = [...canisterIds]; - - canisterIds.forEach((canisterId) => { - canistersMetaData[canisterId] = - metadata.get( - canisterId, - ) as Icrc1Canister; - }); - setCanisterMetaData({ - ...canistersMetaData, - }); - setRealm({ ...realm }); - } catch (e) { - return alert(e); - } - }} - /> -
- )} -
- {realm.tokens?.length && - realm.tokens.map((token) => realmTokenInfo(token))} -
-
-
-

Color Theme

void; post: Post; - allowedTippingCanisterIds: string[]; - canistersMetaData: Record; - externalTips: PostTip[]; - setExternalTips: React.Dispatch>; callback: () => Promise; }) => { - const [selectedTippingCanisterId, setSelectedTippingCanisterId] = - React.useState(CANISTER_ID); + const { token_symbol, token_decimals } = window.backendCache.config; const [tippingAmount, setTippingAmount] = React.useState("0.1"); - const [postAuthor, setPostAuthor] = React.useState(); const [showConfirmation, setShowConfirmation] = React.useState(false); - React.useEffect(() => { - window.api - .query("user", domain(), [post.user.toString()]) - .then(setPostAuthor); - }, []); - - const onTokenSelectionChange = (canisterId: string) => { - setSelectedTippingCanisterId(canisterId); - - const canister = canistersMetaData[canisterId]; - if (!canister) { - return showPopUp( - "error", - `Could not find canister data for ${canisterId}`, - ); - } - setTippingAmount( - (canister.fee / Math.pow(10, canister.decimals)).toFixed( - canister.decimals, - ), - ); - }; - const finalizeTip = async (popUpCallback: () => void) => { try { - const canisterId = selectedTippingCanisterId; - const canister = canistersMetaData[canisterId]; - if (!canister) { - return showPopUp( - "error", - `Could not find canister data for ${canisterId}`, - ); - } - const numericAmount = Number(tippingAmount); if (!numericAmount || isNaN(numericAmount)) return; const amount = Number( - (numericAmount * Math.pow(10, canister.decimals)).toFixed(0), - ); - - if (!postAuthor) - return showPopUp("error", "Could not load post author data."); - - const { token_symbol } = window.backendCache.config; - - // Native token tipping - if (canister.symbol === token_symbol) { - let response = await window.api.call( - "tip", - post.id, - amount, - ); - if ("Err" in response) { - throw new Error(response.Err); - } else await callback(); - - popUpCallback(); - - return; - } - - // ICRC-1 token tipping - let transId = await window.api.icrc_transfer( - Principal.fromText(canisterId), - Principal.fromText(postAuthor.principal), - amount, - canister.fee, - numberToUint8Array(post.id), + (numericAmount * Math.pow(10, token_decimals)).toFixed(0), ); - if (Number.isNaN(transId as number)) { - throw new Error( - transId.toString() || "Something went wrong with transfer!", - ); + const response = await window.api.call("tip", post.id, amount); + if ("Err" in response) { + throw new Error(response.Err); } - - const optimisticPostTip: PostTip = { - amount, - canister_id: canisterId, - index: Number(transId), - sender_id: window.user.id, - }; - setExternalTips([...externalTips, optimisticPostTip]); - + await callback(); popUpCallback(); - - let addTipResponse = await window.api.call<{ - Ok: PostTip; - Err: string; - }>( - "add_external_icrc_transaction", - canisterId, - Number(transId), - post.id, - ); - if ("Err" in (addTipResponse || {}) || !addTipResponse) { - setExternalTips( - externalTips.filter( - ({ canister_id, index }) => - index !== optimisticPostTip.index || - canisterId !== canister_id, - ), - ); - throw new Error( - addTipResponse?.Err || "Could not add tip to post.", - ); - } - - setExternalTips([ - ...externalTips.filter( - ({ index }) => index !== Number(transId), - ), - addTipResponse.Ok, - ]); } catch (e: any) { return showPopUp("error", e?.message || e); } }; - const canister = canistersMetaData[selectedTippingCanisterId]; - return (

- Tip {post.meta.author_name} - with - [ - canisterId, - canistersMetaData[canisterId], - ])} - onSelectionChange={onTokenSelectionChange} - selectedCanisterId={selectedTippingCanisterId} - /> + Tip {post.meta.author_name} with{" "} + {token_symbol}

{ + onChange={(e) => { setTippingAmount(e.target.value); setShowConfirmation(false); }} /> - {showConfirmation && canister && ( + {showConfirmation && (
Transfer{" "} - {Number(tippingAmount).toLocaleString()}{" "} - {canister.symbol} + {Number(tippingAmount).toLocaleString()} {token_symbol} {" "} to ; - disabled?: boolean; - onSelectionChange: (canisterId: string) => void; - selectedCanisterId?: string; -}) => { - const [selectedValue, setSelectedValue] = React.useState( - selectedCanisterId || "", - ); - const [userTokens, setUserTokens] = React.useState< - Array<[string, Icrc1Canister]> - >([]); - - const handleChange = (event: any) => { - const value = (event.target as any).value || CANISTER_ID; - setSelectedValue(value); - onSelectionChange(value); - }; - - const loadData = () => { - const canistersMap = new Map(canisters); - - const userTokenIds = [ - ...(window.user?.wallet_tokens || []), - CANISTER_ID, - ICP_LEDGER, - ]; - const uniqueTokenIds = [...new Set(userTokenIds)]; - setUserTokens( - uniqueTokenIds - .filter((id) => canistersMap.has(id)) - .map((canisterId) => [ - canisterId, - canistersMap.get(canisterId) as Icrc1Canister, - ]), - ); - }; - - React.useEffect(() => { - loadData(); - if (selectedCanisterId) { - setSelectedValue(selectedCanisterId); - } - }, [selectedCanisterId, canisters.map(([id]) => id).toString(), disabled]); - - const renderOptions = ( - canisters: Array<[string, Icrc1Canister]>, - label: string, - ) => { - return ( - - {canisters.map(([canisterId, canisterMeta]) => ( - - ))} - - ); - }; - - return ( - - ); -}; diff --git a/src/frontend/src/types.tsx b/src/frontend/src/types.tsx index eb0ec40a..3e08f224 100644 --- a/src/frontend/src/types.tsx +++ b/src/frontend/src/types.tsx @@ -147,7 +147,6 @@ export type Realm = { last_setting_update: number; revenue: number; posts: PostId[]; - tokens?: string[]; }; export type Meta = { @@ -180,7 +179,6 @@ export type Post = { tree_update: BigInt; meta: Meta; encrypted: boolean; - external_tips?: PostTip[]; hidden_for: UserId[]; }; @@ -400,13 +398,6 @@ export type UserData = { [id: UserId]: string; }; -export interface PostTip { - amount: number; - canister_id: string; - sender_id: number; - index: number; -} - export interface IcExplorerUserTokenInfo { ledgerId: string; symbol: string; From d4a5fabe449de97794d7476cd5f4d8b72992745f Mon Sep 17 00:00:00 2001 From: xqxpx Date: Mon, 27 Apr 2026 09:55:23 +0200 Subject: [PATCH 3/4] improve vm setting --- Makefile | 10 ++++++++++ shell.nix | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/Makefile b/Makefile index 6e45ec8b..d3b343bd 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,16 @@ build: ./build.sh bucket ./build.sh taggr +check: + cargo check --tests + cargo fmt --check + npx tsc --noEmit + npx prettier --check src/frontend/ + +format: + cargo fmt + npx prettier --write src/frontend/ + test: make e2e_build make local_deploy diff --git a/shell.nix b/shell.nix index 2c4e0177..03492846 100644 --- a/shell.nix +++ b/shell.nix @@ -4,5 +4,16 @@ pkgs.mkShell { buildInputs = [ pkgs.podman pkgs.gnumake + pkgs.cargo + pkgs.rustc + pkgs.rustfmt + pkgs.gcc + pkgs.nodejs ]; + + # Bypass the repo's rust-toolchain.toml so the nixpkgs-pinned + # cargo/rustc are used instead of trying to invoke rustup. + shellHook = '' + export RUSTUP_TOOLCHAIN= + ''; } From ea36699fdd3c9cfb36acb1e503c407b0f6e1777a Mon Sep 17 00:00:00 2001 From: xqxpx Date: Mon, 27 Apr 2026 10:40:42 +0200 Subject: [PATCH 4/4] fix testing --- src/backend/env/mod.rs | 1 - src/backend/env/post.rs | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index e1a7bdb8..f60c4122 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -66,7 +66,6 @@ pub const HOUR: u64 = 60 * MINUTE; pub const DAY: u64 = 24 * HOUR; pub const WEEK: u64 = 7 * DAY; pub const MONTH: u64 = 30 * DAY; -pub const YEAR: u64 = 52 * WEEK; pub const MAX_USER_ID: UserId = 9_007_199_254_740_991; // Number.MAX_SAFE_INTEGER in JS diff --git a/src/backend/env/post.rs b/src/backend/env/post.rs index baf0de9f..49819953 100644 --- a/src/backend/env/post.rs +++ b/src/backend/env/post.rs @@ -1085,7 +1085,7 @@ mod tests { assert_eq!(state.posts.len(), 10); // Trigger post archiving archive_cold_posts(state, 5).unwrap(); - assert!(state.memory.health("B").starts_with("boundary=1044B")); + assert!(state.memory.health("B").starts_with("boundary=969B")); assert!(state.memory.health("B").ends_with("segments=0")); // Make sure we have the right numbers in cold and hot memories @@ -1127,7 +1127,7 @@ mod tests { assert!(!Post::get(state, &3).unwrap().archived); assert_eq!(state.posts.len(), 8); assert_eq!(state.memory.posts.len(), 3); - assert!(state.memory.health("B").starts_with("boundary=1044B")); + assert!(state.memory.health("B").starts_with("boundary=969B")); assert!(state.memory.health("B").ends_with("segments=2")); // Archive posts again @@ -1136,7 +1136,7 @@ mod tests { assert_eq!(state.memory.posts.len(), 6); // Segments were reduced, becasue the new post 10 fits into a gap left from one of the // old posts - assert!(state.memory.health("B").starts_with("boundary=1460B")); + assert!(state.memory.health("B").starts_with("boundary=1355B")); assert!(state.memory.health("B").ends_with("segments=1")); }); }