Skip to content

Commit

Permalink
feat: add transfer_username API
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Sep 25, 2024
1 parent 0f6c1cb commit 215f455
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 19 deletions.
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ opt-level = 's'

[workspace.package]
edition = "2021"
version = "2.1.1"
version = "2.2.2"
repository = "https://github.com/ldclabs/ic-panda"
keywords = ["canister", "icp", "panda"]
categories = ["web-programming"]
Expand Down
3 changes: 3 additions & 0 deletions src/ic_message/ic_message.did
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type Result_4 = variant { Ok : CanisterStatusResponse; Err : text };
type Result_5 = variant { Ok : StateInfo; Err : text };
type Result_6 = variant { Ok : blob; Err : text };
type Result_7 = variant { Ok : vec text; Err : text };
type Result_8 = variant { Ok : text; Err : text };
type StateInfo = record {
latest_usernames : vec text;
managers : vec principal;
Expand Down Expand Up @@ -184,10 +185,12 @@ service : (opt ChainArgs) -> {
register_username : (text, opt text) -> (Result_3);
save_channel_kek : (ChannelKEKInput) -> (Result);
search_username : (text) -> (Result_7) query;
transfer_username : (principal) -> (Result);
update_my_ecdh : (blob, blob) -> (Result);
update_my_image : (text) -> (Result);
update_my_kv : (UpdateKVInput) -> (Result);
update_my_name : (text) -> (Result_3);
validate2_admin_update_price : (UpdatePriceInput) -> (Result_8);
validate_admin_add_canister : (CanisterKind, principal) -> (Result);
validate_admin_add_managers : (vec principal) -> (Result);
validate_admin_collect_token : (Account, nat) -> (Result);
Expand Down
5 changes: 5 additions & 0 deletions src/ic_message/src/api_admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ fn validate_admin_update_price(args: types::UpdatePriceInput) -> Result<(), Stri
})?;
Ok(())
}
#[ic_cdk::update]
fn validate2_admin_update_price(args: types::UpdatePriceInput) -> Result<String, String> {
validate_admin_update_price(args)?;
Ok("ok".to_string())
}

#[ic_cdk::update]
fn validate_admin_collect_token(_user: Account, amount: Nat) -> Result<(), String> {
Expand Down
12 changes: 12 additions & 0 deletions src/ic_message/src/api_update.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use candid::Principal;
use ic_cose_types::{cose::encrypt0::try_decode_encrypt0, validate_key, MILLISECONDS};
use ic_message_types::{
channel::{ChannelInfo, ChannelKEKInput, CreateChannelInput},
Expand Down Expand Up @@ -34,6 +35,17 @@ async fn register_username(username: String, name: Option<String>) -> Result<Use
store::user::register_username(caller, username.clone(), name.unwrap_or(username), now_ms).await
}

#[ic_cdk::update(guard = "is_authenticated")]
async fn transfer_username(to: Principal) -> Result<(), String> {
let caller = ic_cdk::caller();
if caller == to {
Err("cannot transfer to self".to_string())?;
}

let now_ms = ic_cdk::api::time() / MILLISECONDS;
store::user::transfer_username(caller, to, now_ms).await
}

#[ic_cdk::update(guard = "is_authenticated")]
async fn update_my_name(name: String) -> Result<UserInfo, String> {
if name.is_empty() {
Expand Down
148 changes: 147 additions & 1 deletion src/ic_message/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,17 @@ pub mod user {
return Err("invalid username length".to_string());
}

let has_username = USER_STORE.with(|r| {
r.borrow()
.get(&caller)
.map(|u| u.username.is_some())
.unwrap_or_default()
});

if has_username {
return Err("caller already has username".to_string());
}

NAME_STORE.with(|r| {
let mut m = r.borrow_mut();
match m.get(&ln) {
Expand Down Expand Up @@ -552,6 +563,7 @@ pub mod user {
ic_cdk::api::set_certified_data(s.root_hash().as_slice());
});

// the user's namespace maybe exists, but we don't care about the result
let _: Result<NamespaceInfo, String> = call(
cose_canister,
"admin_create_namespace",
Expand All @@ -572,7 +584,9 @@ pub mod user {
let mut m = r.borrow_mut();
match m.get(&caller) {
Some(mut user) => {
user.cose_canister = Some(cose_canister);
if user.cose_canister.is_none() {
user.cose_canister = Some(cose_canister);
}
user.username = Some(username);
m.insert(caller, user.clone());
user.into_info(caller)
Expand Down Expand Up @@ -601,6 +615,138 @@ pub mod user {
Ok(info)
}

pub async fn transfer_username(
caller: Principal,
to: Principal,
now_ms: u64,
) -> Result<(), String> {
let (cose_canister, profile_canister) = state::with(|s| {
(
s.cose_canisters.last().cloned(),
s.profile_canisters.last().cloned(),
)
});
let cose_canister = cose_canister.ok_or_else(|| "no COSE canister".to_string())?;
let profile_canister = profile_canister.ok_or_else(|| "no profile canister".to_string())?;

let username = USER_STORE.with(|r| {
r.borrow()
.get(&caller)
.ok_or_else(|| "caller not found".to_string())?
.username
.ok_or_else(|| "caller has no username".to_string())
})?;

let ln = username.to_lowercase();
let (new_profile, new_cose) = NAME_STORE.with(|r| {
let mut m = r.borrow_mut();
match m.get(&ln) {
Some(owner) => {
if owner != caller {
Err("username not owned by caller".to_string())
} else {
let rt = USER_STORE.with(|r| {
let mut new_cose = false;
let mut new_profile = false;
let mut m = r.borrow_mut();
match m.get(&to) {
Some(mut user) => {
if let Some(username) = user.username {
Err(format!(
"{} already has username {}",
to.to_text(),
username
))?;
}
user.username = Some(username.clone());
if user.cose_canister.is_none() {
new_cose = true;
user.cose_canister = Some(cose_canister);
}
m.insert(to, user);
}
None => {
new_cose = true;
new_profile = true;
m.insert(
to,
User {
name: username.clone(),
image: "".to_string(),
profile_canister: profile_canister,
cose_canister: Some(cose_canister),
username: Some(username.clone()),
},
);
}
}

if let Some(mut user) = m.get(&caller) {
user.username = None;
m.insert(caller, user.clone());
}
Ok::<(bool, bool), String>((new_profile, new_cose))
})?;
m.insert(ln.clone(), to);
Ok(rt)
}
}
None => Err("username not found".to_string()),
}
})?;

state::with_mut(|s| {
let blk = NameBlock {
height: s.next_block_height,
phash: s.next_block_phash,
name: ln,
user: to,
from: Some(caller),
value: 0,
timestamp: now_ms,
};
let blk = to_cbor_bytes(&blk);
s.next_block_height += 1;
s.next_block_phash = sha3_256(&blk).into();
NAME_BLOCKS.with(|r| {
r.borrow_mut()
.append(&blk)
.expect("failed to append NameBlock");
});
ic_cdk::api::set_certified_data(s.root_hash().as_slice());
});

if new_profile {
let _: Result<(), String> = call(
profile_canister,
"admin_upsert_profile",
(to, None::<(Principal, u64)>),
0,
)
.await?;
}

if new_cose {
let _: Result<NamespaceInfo, String> = call(
cose_canister,
"admin_create_namespace",
(CreateNamespaceInput {
name: to.to_text().replace("-", "_"),
visibility: 0,
desc: Some(format!("name: {}", username)),
max_payload_size: Some(1024),
managers: BTreeSet::from([ic_cdk::id()]),
auditors: BTreeSet::from([to]),
users: BTreeSet::from([to]),
},),
0,
)
.await?;
}

Ok(())
}

pub fn search_username(prefix: String) -> Vec<String> {
state::with(|s| {
if prefix.len() <= 7 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ type Result_4 = variant { Ok : CanisterStatusResponse; Err : text };
type Result_5 = variant { Ok : StateInfo; Err : text };
type Result_6 = variant { Ok : blob; Err : text };
type Result_7 = variant { Ok : vec text; Err : text };
type Result_8 = variant { Ok : text; Err : text };
type StateInfo = record {
latest_usernames : vec text;
managers : vec principal;
Expand Down Expand Up @@ -184,10 +185,12 @@ service : (opt ChainArgs) -> {
register_username : (text, opt text) -> (Result_3);
save_channel_kek : (ChannelKEKInput) -> (Result);
search_username : (text) -> (Result_7) query;
transfer_username : (principal) -> (Result);
update_my_ecdh : (blob, blob) -> (Result);
update_my_image : (text) -> (Result);
update_my_kv : (UpdateKVInput) -> (Result);
update_my_name : (text) -> (Result_3);
validate2_admin_update_price : (UpdatePriceInput) -> (Result_8);
validate_admin_add_canister : (CanisterKind, principal) -> (Result);
validate_admin_add_managers : (vec principal) -> (Result);
validate_admin_collect_token : (Account, nat) -> (Result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ export type Result_6 = { 'Ok' : Uint8Array | number[] } |
{ 'Err' : string };
export type Result_7 = { 'Ok' : Array<string> } |
{ 'Err' : string };
export type Result_8 = { 'Ok' : string } |
{ 'Err' : string };
export interface StateInfo {
'latest_usernames' : Array<string>,
'managers' : Array<Principal>,
Expand Down Expand Up @@ -218,13 +220,15 @@ export interface _SERVICE {
'register_username' : ActorMethod<[string, [] | [string]], Result_3>,
'save_channel_kek' : ActorMethod<[ChannelKEKInput], Result>,
'search_username' : ActorMethod<[string], Result_7>,
'transfer_username' : ActorMethod<[Principal], Result>,
'update_my_ecdh' : ActorMethod<
[Uint8Array | number[], Uint8Array | number[]],
Result
>,
'update_my_image' : ActorMethod<[string], Result>,
'update_my_kv' : ActorMethod<[UpdateKVInput], Result>,
'update_my_name' : ActorMethod<[string], Result_3>,
'validate2_admin_update_price' : ActorMethod<[UpdatePriceInput], Result_8>,
'validate_admin_add_canister' : ActorMethod<
[CanisterKind, Principal],
Result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export const idlFactory = ({ IDL }) => {
'upsert_kv' : IDL.Vec(IDL.Tuple(IDL.Text, IDL.Vec(IDL.Nat8))),
'remove_kv' : IDL.Vec(IDL.Text),
});
const Result_8 = IDL.Variant({ 'Ok' : IDL.Text, 'Err' : IDL.Text });
return IDL.Service({
'admin_add_canister' : IDL.Func(
[CanisterKind, IDL.Principal],
Expand Down Expand Up @@ -258,6 +259,7 @@ export const idlFactory = ({ IDL }) => {
),
'save_channel_kek' : IDL.Func([ChannelKEKInput], [Result], []),
'search_username' : IDL.Func([IDL.Text], [Result_7], ['query']),
'transfer_username' : IDL.Func([IDL.Principal], [Result], []),
'update_my_ecdh' : IDL.Func(
[IDL.Vec(IDL.Nat8), IDL.Vec(IDL.Nat8)],
[Result],
Expand All @@ -266,6 +268,11 @@ export const idlFactory = ({ IDL }) => {
'update_my_image' : IDL.Func([IDL.Text], [Result], []),
'update_my_kv' : IDL.Func([UpdateKVInput], [Result], []),
'update_my_name' : IDL.Func([IDL.Text], [Result_3], []),
'validate2_admin_update_price' : IDL.Func(
[UpdatePriceInput],
[Result_8],
[],
),
'validate_admin_add_canister' : IDL.Func(
[CanisterKind, IDL.Principal],
[Result],
Expand Down
5 changes: 5 additions & 0 deletions src/ic_panda_frontend/src/lib/canisters/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ export class MessageCanisterAPI {
return unwrapResult(res, 'call register_username failed')
}

async transfer_username(to: Principal): Promise<null> {
const res = await this.actor.transfer_username(to)
return unwrapResult(res, 'call transfer_username failed')
}

async save_channel_kek(input: ChannelKEKInput): Promise<null> {
const res = await this.actor.save_channel_kek(input)
return unwrapResult(res, 'call save_channel_kek failed')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,11 @@
// no scroll
if (elemChat?.scrollTop == 0) {
const msg = $messageFeed.at(-1)
if (msg && msg.id > lastRead && msg.id !== msg.pid) {
if (
msg &&
msg.id > channelInfo.my_setting.last_read &&
msg.id !== msg.pid
) {
lastRead = msg.id
debouncedUpdateMyLastRead()
}
Expand Down
Loading

0 comments on commit 215f455

Please sign in to comment.