Skip to content

Commit

Permalink
Merge branch 'master' into display_name
Browse files Browse the repository at this point in the history
  • Loading branch information
hpeebles authored Dec 4, 2023
2 parents 6e49eb2 + 81d08c6 commit e8890fc
Show file tree
Hide file tree
Showing 31 changed files with 336 additions and 130 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ async fn process_action(action: Action) {
token: Cryptocurrency::CKBTC,
amount: amount as u128,
fee: 10,
from: icrc1::CryptoAccount::Account(from),
to: icrc1::CryptoAccount::Account(Account::from(Principal::from(user_id))),
from: from.into(),
to: Account::from(Principal::from(user_id)).into(),
memo: None,
created: now_nanos,
block_index: block_index.0.try_into().unwrap(),
Expand Down
2 changes: 2 additions & 0 deletions backend/canisters/escrow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- Introduce the Escrow canister for supporting P2P trades ([#4903](https://github.com/open-chat-labs/open-chat/pull/4903))
- Implement `create_offer` and `notify_deposit` ([#4904](https://github.com/open-chat-labs/open-chat/pull/4904))
- Transfer out funds once trade is complete ([#4906](https://github.com/open-chat-labs/open-chat/pull/4906))
- Implement `cancel_offer` ([#4907](https://github.com/open-chat-labs/open-chat/pull/4907))
1 change: 1 addition & 0 deletions backend/canisters/escrow/api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ candid = { workspace = true }
candid_gen = { path = "../../../libraries/candid_gen" }
icrc-ledger-types = { workspace = true }
serde = { workspace = true }
sha256 = { path = "../../../libraries/sha256" }
types = { path = "../../../libraries/types" }
11 changes: 5 additions & 6 deletions backend/canisters/escrow/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use candid::Principal;
use icrc_ledger_types::icrc1::account::Subaccount;
use sha256::sha256;
use types::UserId;

mod lifecycle;
Expand All @@ -11,10 +12,8 @@ pub use queries::*;
pub use updates::*;

pub fn deposit_subaccount(user_id: UserId, offer_id: u32) -> Subaccount {
let mut subaccount = [0; 32];
let principal = Principal::from(user_id);
let user_id_bytes = principal.as_slice();
subaccount[..user_id_bytes.len()].copy_from_slice(user_id_bytes);
subaccount[28..].copy_from_slice(&offer_id.to_be_bytes());
subaccount
let mut bytes = Vec::new();
bytes.extend_from_slice(Principal::from(user_id).as_slice());
bytes.extend_from_slice(&offer_id.to_be_bytes());
sha256(&bytes)
}
15 changes: 15 additions & 0 deletions backend/canisters/escrow/api/src/updates/cancel_offer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Args {
pub offer_id: u32,
}

#[derive(Serialize, Deserialize, Debug)]
pub enum Response {
Success,
OfferAlreadyAccepted,
OfferExpired,
OfferNotFound,
NotAuthorized,
}
1 change: 1 addition & 0 deletions backend/canisters/escrow/api/src/updates/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod cancel_offer;
pub mod create_offer;
pub mod notify_deposit;
1 change: 1 addition & 0 deletions backend/canisters/escrow/api/src/updates/notify_deposit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub enum Response {
Success,
BalanceTooLow(BalanceTooLowResult),
OfferAlreadyAccepted,
OfferCancelled,
OfferExpired,
OfferNotFound,
InternalError(String),
Expand Down
102 changes: 102 additions & 0 deletions backend/canisters/escrow/impl/src/jobs/make_pending_payments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use crate::model::pending_payments_queue::{PendingPayment, PendingPaymentReason};
use crate::{mutate_state, RuntimeState};
use candid::Principal;
use escrow_canister::deposit_subaccount;
use ic_cdk_timers::TimerId;
use icrc_ledger_types::icrc1::account::Account;
use icrc_ledger_types::icrc1::transfer::TransferArg;
use std::cell::Cell;
use std::time::Duration;
use tracing::{error, trace};
use types::icrc1::CompletedCryptoTransaction;
use types::CanisterId;
use utils::time::NANOS_PER_MILLISECOND;

thread_local! {
static TIMER_ID: Cell<Option<TimerId>> = Cell::default();
}

pub(crate) fn start_job_if_required(state: &RuntimeState) -> bool {
if TIMER_ID.get().is_none() && !state.data.pending_payments_queue.is_empty() {
let timer_id = ic_cdk_timers::set_timer_interval(Duration::ZERO, run);
TIMER_ID.set(Some(timer_id));
trace!("'make_pending_payments' job started");
true
} else {
false
}
}

pub fn run() {
if let Some(pending_payment) = mutate_state(|state| state.data.pending_payments_queue.pop()) {
ic_cdk::spawn(process_payment(pending_payment));
} else if let Some(timer_id) = TIMER_ID.take() {
ic_cdk_timers::clear_timer(timer_id);
trace!("'make_pending_payments' job stopped");
}
}

async fn process_payment(pending_payment: PendingPayment) {
let from_user = match pending_payment.reason {
PendingPaymentReason::Trade(other_user_id) => other_user_id,
PendingPaymentReason::Refund => pending_payment.user_id,
};
let created_at_time = pending_payment.timestamp * NANOS_PER_MILLISECOND;

let args = TransferArg {
from_subaccount: Some(deposit_subaccount(from_user, pending_payment.offer_id)),
to: Principal::from(pending_payment.user_id).into(),
fee: Some(pending_payment.token_info.fee.into()),
created_at_time: Some(created_at_time),
memo: None,
amount: pending_payment.amount.into(),
};

match make_payment(pending_payment.token_info.ledger, &args).await {
Ok(block_index) => {
mutate_state(|state| {
if let Some(offer) = state.data.offers.get_mut(pending_payment.offer_id) {
let transfer = CompletedCryptoTransaction {
ledger: pending_payment.token_info.ledger,
token: pending_payment.token_info.token,
amount: pending_payment.amount,
from: Account {
owner: state.env.canister_id(),
subaccount: args.from_subaccount,
}
.into(),
to: Account::from(Principal::from(pending_payment.user_id)).into(),
fee: pending_payment.token_info.fee,
memo: None,
created: created_at_time,
block_index,
};
offer.transfers_out.push(transfer);
}
});
}
Err(retry) => {
if retry {
mutate_state(|state| {
state.data.pending_payments_queue.push(pending_payment);
start_job_if_required(state);
});
}
}
}
}

// Error response contains a boolean stating if the transfer should be retried
async fn make_payment(ledger_canister_id: CanisterId, args: &TransferArg) -> Result<u64, bool> {
match icrc_ledger_canister_c2c_client::icrc1_transfer(ledger_canister_id, args).await {
Ok(Ok(block_index)) => Ok(block_index.0.try_into().unwrap()),
Ok(Err(transfer_error)) => {
error!(?transfer_error, ?args, "Transfer failed");
Err(false)
}
Err(error) => {
error!(?error, ?args, "Transfer failed");
Err(true)
}
}
}
6 changes: 5 additions & 1 deletion backend/canisters/escrow/impl/src/jobs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use crate::RuntimeState;

pub(crate) fn start(_state: &RuntimeState) {}
pub mod make_pending_payments;

pub(crate) fn start(state: &RuntimeState) {
make_pending_payments::start_job_if_required(state);
}
3 changes: 3 additions & 0 deletions backend/canisters/escrow/impl/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::model::offers::Offers;
use crate::model::pending_payments_queue::PendingPaymentsQueue;
use canister_state_macros::canister_state;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
Expand Down Expand Up @@ -45,6 +46,7 @@ impl RuntimeState {
#[derive(Serialize, Deserialize)]
struct Data {
pub offers: Offers,
pub pending_payments_queue: PendingPaymentsQueue,
pub cycles_dispenser_canister_id: CanisterId,
pub rng_seed: [u8; 32],
pub test_mode: bool,
Expand All @@ -54,6 +56,7 @@ impl Data {
pub fn new(cycles_dispenser_canister_id: CanisterId, test_mode: bool) -> Data {
Data {
offers: Offers::default(),
pending_payments_queue: PendingPaymentsQueue::default(),
cycles_dispenser_canister_id,
rng_seed: [0; 32],
test_mode,
Expand Down
1 change: 1 addition & 0 deletions backend/canisters/escrow/impl/src/model/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod offers;
pub mod pending_payments_queue;
10 changes: 5 additions & 5 deletions backend/canisters/escrow/impl/src/model/offers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use types::{CompletedCryptoTransaction, TimestampMillis, TokenInfo, UserId};
use types::{icrc1::CompletedCryptoTransaction, TimestampMillis, TokenInfo, UserId};

#[derive(Serialize, Deserialize, Default)]
pub struct Offers {
Expand Down Expand Up @@ -29,11 +29,11 @@ pub struct Offer {
pub token1: TokenInfo,
pub amount1: u128,
pub expires_at: TimestampMillis,
pub cancelled_at: Option<TimestampMillis>,
pub accepted_by: Option<(UserId, TimestampMillis)>,
pub token0_received: bool,
pub token1_received: bool,
pub transfer_out0: Option<CompletedCryptoTransaction>,
pub transfer_out1: Option<CompletedCryptoTransaction>,
pub transfers_out: Vec<CompletedCryptoTransaction>,
}

impl Offer {
Expand All @@ -47,11 +47,11 @@ impl Offer {
token1: args.output_token,
amount1: args.output_amount,
expires_at: args.expires_at,
cancelled_at: None,
accepted_by: None,
token0_received: false,
token1_received: false,
transfer_out0: None,
transfer_out1: None,
transfers_out: Vec::new(),
}
}
}
38 changes: 38 additions & 0 deletions backend/canisters/escrow/impl/src/model/pending_payments_queue.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use types::{TimestampMillis, TokenInfo, UserId};

#[derive(Serialize, Deserialize, Default)]
pub struct PendingPaymentsQueue {
pending_payments: VecDeque<PendingPayment>,
}

impl PendingPaymentsQueue {
pub fn push(&mut self, pending_payment: PendingPayment) {
self.pending_payments.push_back(pending_payment);
}

pub fn pop(&mut self) -> Option<PendingPayment> {
self.pending_payments.pop_front()
}

pub fn is_empty(&self) -> bool {
self.pending_payments.is_empty()
}
}

#[derive(Serialize, Deserialize)]
pub struct PendingPayment {
pub user_id: UserId,
pub timestamp: TimestampMillis,
pub token_info: TokenInfo,
pub amount: u128,
pub offer_id: u32,
pub reason: PendingPaymentReason,
}

#[derive(Serialize, Deserialize, Clone, Copy)]
pub enum PendingPaymentReason {
Trade(UserId),
Refund,
}
31 changes: 31 additions & 0 deletions backend/canisters/escrow/impl/src/updates/cancel_offer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use crate::{mutate_state, RuntimeState};
use canister_api_macros::update_msgpack;
use canister_tracing_macros::trace;
use escrow_canister::cancel_offer::{Response::*, *};

#[update_msgpack]
#[trace]
fn cancel_offer(args: Args) -> Response {
mutate_state(|state| cancel_offer_impl(args, state))
}

fn cancel_offer_impl(args: Args, state: &mut RuntimeState) -> Response {
if let Some(offer) = state.data.offers.get_mut(args.offer_id) {
let user_id = state.env.caller().into();
let now = state.env.now();
if offer.created_by != user_id {
NotAuthorized
} else if offer.accepted_by.is_some() {
OfferAlreadyAccepted
} else if offer.expires_at < now {
OfferExpired
} else {
if offer.cancelled_at.is_none() {
offer.cancelled_at = Some(now);
}
Success
}
} else {
OfferNotFound
}
}
1 change: 1 addition & 0 deletions backend/canisters/escrow/impl/src/updates/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod cancel_offer;
pub mod create_offer;
pub mod notify_deposit;
pub mod wallet_receive;
Loading

0 comments on commit e8890fc

Please sign in to comment.