Ckusdc custodial wallet

Title:

How to Enable ckUSDC Deposit and Withdrawal in a Custodial Wallet Using Internet Computer (IC)?


Question Body:

I’m building a custodial wallet on the Internet Computer (IC) where users can deposit and withdraw ckUSDC. The system allows users to deposit USDC into their account, make off-chain transactions with other users, and withdraw their earnings to an external wallet like Coinbase or MetaMask.

To achieve this, I’m following the icp-pay repo as a reference. Here’s my current approach for implementing the deposit and withdraw functionality:

Key points

  1. Error Simplification: Use consistent and clear error handling with informative messages for developers and end-users.
  2. Abstraction: Create helper functions to encapsulate repetitive tasks like decoding responses and making canister calls.
  3. Comments: Add comments to explain the purpose of each function and its parameters.
  4. Type Validation: Ensure types are correctly mapped during canister calls to avoid decoding errors.
  5. Clear Interfaces: Provide a function get_ckusdc_deposit_address that gives developers a clear and simple API to interact with.
use crate::wallet::error::{Error, USDCResult};
use crate::websocket::{NoteContent, Notification};
use crate::workspaces::{
    estimate_transaction_fees, nat_to_u256, nat_to_u64, validate_caller_not_anonymous,
    EthereumNetwork, EVM_RPC,
};
use crate::{assert_token_symbol_length, parse_eth_address, wallet, UserToken};
use crate::{CPayment, ExchangeType, PaymentStatus, Wallet};
use alloy_consensus::{SignableTransaction, TxEip1559, TxEnvelope};
use alloy_primitives::{address, hex, Signature, TxKind, U256};
use candid::CandidType;
use candid::{func, Nat, Principal};
use ethers_core::types::Res;
use evm_rpc_canister_types::{
    BlockTag, EthMainnetService, EthSepoliaService, EvmRpcCanister, GetTransactionCountArgs,
    GetTransactionCountResult, MultiGetTransactionCountResult, RequestResult, RpcService,
};
use ic_cdk::caller;
use ic_cdk_macros::update;
use icrc_ledger_types::icrc1::account::Account;
use icrc_ledger_types::icrc1::transfer::BlockIndex;
use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs;
use num::Num;
use serde_json::{self};
use std::time;

async fn get_user_balance() -> USDCResult<Nat> {
    let args = Account {
        owner: caller(),
        subaccount: None,
    };
    ic_cdk::call::<(Account,), (USDCResult<Nat>,)>(
        Principal::from_text("xevnm-gaaaa-aaaar-qafnq-cai").unwrap(),
        "icrc1_balance_of",
        (args,),
    )
    .await
    .map_err(|(_, message)| Error::IcCdkError { message })?
    .0
    .map_err(|e| Error::IcCdkError {
        message: format!("{:?}", e),
    })
}

async fn transfer_from(amount: Nat, from: Principal, to: Principal) -> USDCResult<BlockIndex> {
    let args = TransferFromArgs {
        spender_subaccount: None,
        from: Account {
            owner: from,
            subaccount: None,
        },
        to: Account {
            owner: to,
            subaccount: None,
        },
        amount,
        fee: None,
        memo: None,
        created_at_time: None,
    };
    ic_cdk::call::<(TransferFromArgs,), (USDCResult<BlockIndex>,)>(
        Principal::from_text("xevnm-gaaaa-aaaar-qafnq-cai").unwrap(),
        "icrc2_transfer_from",
        (args,),
    )
    .await
    .map_err(|(_, message)| Error::IcCdkError { message })?
    .0
    .map_err(|e| Error::IcCdkError {
        message: format!("{:?}", e),
    })
}

#[update]
async fn deposit_ckusdt() -> Result<Nat, Error> {
    let balance = get_user_balance().await?;
    if balance > Nat::from(0 as u64) {
        transfer_from(balance.clone(), caller(), ic_cdk::id()).await?;
        let mut wallet = Wallet::get(caller());
        wallet
            .deposit(
                nat_to_u64(balance.clone()) as f64,
                "ExternalWallet".to_string(),
                ExchangeType::Deposit,
            )
            .map_err(|e| Error::IcCdkError {
                message: format!("{:?}", e),
            })?;
        let message = format!("You received {} CKUSD", balance);
        Notification::new(
            format!("{:?}", time::SystemTime::now()),
            caller(),
            NoteContent::ReceivedDeposit(message),
        )
        .save();
    }
    Ok(balance)
}

#[update]
async fn withdraw_ckusdt(amount: u64, address: String) -> Result<Nat, Error> {
    let balance = get_user_balance().await?;
    if Nat::from(amount.clone()) >= balance {
        transfer_from(
            Nat::from(amount.clone() as u64),
            ic_cdk::id(),
            Principal::from_text(address.clone()).unwrap(),
        )
        .await?;
        let mut wallet = Wallet::get(caller());
        wallet
            .withdraw(amount as f64, address, ExchangeType::Withdraw)
            .map_err(|e| Error::IcCdkError { message: e })?;
    }
    Ok(balance)
}

Issue:
When I call get_user_balance, I get the following error:

css
Copy code
Err(IcCdkError { message: “failed to decode canister response as (core::result::Result<candid::types::number::Nat, alloc::string::String>,): Fail to decode argument 0 from nat to variant { Ok : nat; Err : text }” })


also i got error : “Canister xevnm-gaaaa-aaaar-qafnq-cai not found”

can you quickly confirm that you aim to achieve following things:

  • let users deposit ckUSDC to the dapp specific user principal
  • fetch & display ckUSDC balances for each user
  • let users do something in your dapp to lock ckUSDC in the container backend and pay/release ckUSDC to other users (who earn the ckUSDC) within the dapp
    • this is out of scope of the question here, right?
  • let users withdraw the ckUSDC from their dapp principal to any principal that they provide

is that correct?

Yes that is correct, in general it is a custodial wallet .