//! # Unit P2P Pallet
//! <!-- Original author of paragraph: @matthieu
//!
//! ## Overview
//!
//! Pallet that allows a user to trade fiat using traditional financial systems and cryptocurrencies
//! in a decentralized manner in Unit Network.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Allow sellers to offer cryptos in exchange for fiat.
//! * Buyers can place orders to purchase the cryptocurrencies.
//! * The buyer can specify that the transfer has been made using the instructions in the order.
//! * The seller can confirm if he received the funds or not.
//!
//! ## Interface
//!
//! ### Permissionless Functions
//!
//! - `create_order`: Create order to exchange cryptocurrencies for fiat.
//! - `set_order`: A buyer can indicate the desire to buy.
//!
//! ### Privileged Functions
//!
//! - `mark_transfer_made`: A buyer can indicate that he has already transferred the funds.
//! - `confirm_transfer_received`: The seller can indicate if he received the funds and transfer the cryptos.
//! - `transfer_not_received`: The seller can indicate that he didn't receive the transfer from the buyer.
//! - `reclaim_order_funds`: The seller can withdraw funds from orders that haven't been completed.
//! - `modify_order_details`: The seller can edit the intructions that the buyers needs to follow.
//! - `remove_order_amount`: The seller can withdraw cryptos from the available amount of the order.
//!
//!//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;
use sp_std::{ vec, vec::Vec };
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod types;
pub use types::{ Buyer, Order, OrderStatus };
use traits::subaccounts::{ SubAccounts, AccountOrigin };
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
pallet_prelude::*,
sp_runtime::{
traits::{ AccountIdConversion, Zero },
Saturating,
FixedPointOperand,
SaturatedConversion,
},
traits::{ fungibles, fungibles::*, Time, tokens::{ Preservation::Expendable, Balance } },
PalletId,
};
use frame_system::pallet_prelude::*;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Type to access the Assets Pallet.
type Fungibles: fungibles::Inspect<
Self::AccountId,
AssetId = Self::AssetId,
Balance = Self::AssetBalance
> + //, AssetId = u32> // Hash this
fungibles::Mutate<Self::AccountId> +
fungibles::metadata::Inspect<Self::AccountId> +
fungibles::Create<Self::AccountId>;
type AssetId: Member +
Parameter +
Copy +
MaybeSerializeDeserialize +
MaxEncodedLen +
Default +
Zero +
From<u32> +
Into<u32>;
type AssetBalance: Balance +
FixedPointOperand +
MaxEncodedLen +
MaybeSerializeDeserialize +
TypeInfo;
type PalletId: Get<PalletId>;
type Time: Time;
#[pallet::constant]
type MaxMessageLength: Get<u32>;
/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
#[pallet::constant]
type OrderDuration: Get<u32>;
// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type ProfileBenchmark;
}
// Balance type
pub type BalanceOf<T> = <T as Config>::AssetBalance;
// AssetId type
pub type AssetId<T> = <T as Config>::AssetId;
/// Orders that sellers created.
#[pallet::storage]
#[pallet::getter(fn all_orders)]
pub(super) type AllOrders<T: Config> = StorageMap<
_,
Blake2_128Concat,
u32,
Order<T::AccountId, AssetId<T>, BalanceOf<T>, u64, BlockNumberFor<T>, T::MaxMessageLength>
>;
/// Store the last order for a seller for a token
#[pallet::storage]
#[pallet::getter(fn order_user_tokenid)]
pub(super) type OrderUserTokenId<T: Config> = StorageMap<
_,
Blake2_128Concat,
(T::AccountId, AssetId<T>),
Order<T::AccountId, AssetId<T>, BalanceOf<T>, u64, BlockNumberFor<T>, T::MaxMessageLength>
>;
/// Storage to keep track of the orders where a user bought.
#[pallet::storage]
#[pallet::getter(fn orders)]
pub(super) type OrdersByUser<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
Vec<u32>,
ValueQuery
>;
/// Reputation of the sellers
#[pallet::storage]
#[pallet::getter(fn reputation)]
pub(super) type Reputation<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
(u32, u32)
/* The first one is the number of transfers received, the second is the number of
* orders succesfully done */
>;
/// Counter for orders.
#[pallet::storage]
#[pallet::getter(fn next_order_id)]
pub(super) type NextOrderId<T: Config> = StorageValue<_, u32, ValueQuery>;
// Pallets use events to inform users when important changes are made.
// https://docs.substrate.io/main-docs/build/events-errors/
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
OrderCreated {
who: T::AccountId,
token_id: AssetId<T>,
amount: BalanceOf<T>,
},
}
// Errors inform users that something went wrong.
#[pallet::error]
#[derive(PartialEq)]
pub enum Error<T> {
/// Cannot create a new order until the active one finishes.
OrderInProgress,
/// Order don't exist.
OrderNotFound,
/// Trying to purchase more than available.
NotEnoughAvailableAmount,
/// Order has expired
OrderExpired,
/// Cannot buy more than once for an order.
CanOnlyOneOrder,
/// Buyer is not registered in the order.
BuyerNotFound,
/// There is no buyers for this order.
NoBuyers,
/// The caller is not the creator of the order.
NotSeller,
/// If the transfer has not beed made by the buyer.
TransferNotMarked,
/// Time to confirm a transfer has expired.
TimeLimitExceeded,
/// Cannot reclaim the cryptos for the order yet.
ReclaimTimeNotMet,
/// Seller needs to create an order.
OrderNotExists,
/// Time of the order has finished.
TimeFinished,
/// Cannot buy in your own order.
YouAreTheSeller,
}
// Dispatchable functions allows users to interact with the pallet and invoke state changes.
#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// The origin can create an order.
///
/// The amount of tokens offered will be locked.
/// The order will be valid for a number of blocks. See 'OrderDuration' associated type.
/// Can only have one order active per token.
///
/// Parameters:
/// - `token_id`: The asset id being offered.
/// - `amount`: The amount of tokens that can be purchased for users.
/// - `channel`: The channel where the origin wants to receive the funds.
/// - `instructions` : More instructions on how to send funds.
#[pallet::call_index(1)]
pub fn create_order(
origin: OriginFor<T>,
token_id: AssetId<T>,
amount: BalanceOf<T>,
channel: BoundedVec<u8, T::MaxMessageLength>,
instructions: BoundedVec<u8, T::MaxMessageLength>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;
let p2p_id = Self::account_id(); // Get the account/id of the p2p pallet
T::Fungibles::transfer(
token_id.clone(),
&who.clone(),
&p2p_id.clone(),
amount.clone(),
Expendable
)?; // Transfer funds to the p2p account
let new_id = NextOrderId::<T>::get();
if
let Some(existing_order) = OrderUserTokenId::<T>::get((
who.clone(),
token_id.clone(),
))
{
if <frame_system::Pallet<T>>::block_number() < existing_order.valid_until {
return Err(Error::<T>::OrderInProgress.into());
} else {
// Create an order if time ended
let order = Order {
id: new_id,
seller: who.clone(),
token_id: token_id.clone(),
amount: amount.clone(),
available_amount: amount,
buyers: None,
channel: channel.clone(),
instructions: instructions.clone(),
created_at: T::Time::now().saturated_into::<u64>(),
last_time_action: T::Time::now().saturated_into::<u64>(),
valid_until: <frame_system::Pallet<T>>::block_number() +
T::OrderDuration::get().into(),
};
AllOrders::<T>::insert(new_id, &order);
NextOrderId::<T>::put(new_id + 1);
OrdersByUser::<T>::mutate(&who, |order_ids| order_ids.push(new_id));
OrderUserTokenId::<T>::insert((who.clone(), token_id.clone()), order);
}
} else {
// Orelse, new one
let order = Order {
id: new_id,
seller: who.clone(),
token_id: token_id.clone(),
amount: amount.clone(),
available_amount: amount,
buyers: None,
channel: channel.clone(),
instructions: instructions.clone(),
created_at: T::Time::now().saturated_into::<u64>(),
last_time_action: T::Time::now().saturated_into::<u64>(),
valid_until: <frame_system::Pallet<T>>::block_number() +
T::OrderDuration::get().into(),
};
AllOrders::<T>::insert(new_id, &order);
NextOrderId::<T>::put(new_id + 1);
OrdersByUser::<T>::mutate(&who, |order_ids| order_ids.push(new_id));
OrderUserTokenId::<T>::insert((who.clone(), token_id.clone()), order);
}
Self::deposit_event(Event::OrderCreated { who, token_id, amount });
Ok(())
}
/// The origin can set an response to a particular order.
///
/// The order should be in progress.
/// Can only purchase once in an order.
/// The creator of an order can't buy.
///
/// Parameters:
/// - `id`: order id.
/// - `requested_amount`: Amount the caller wants to buy.
#[pallet::call_index(2)]
pub fn set_order(
origin: OriginFor<T>,
id: u32,
requested_amount: BalanceOf<T>
) -> DispatchResult {
let mut buyer = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
buyer = T::SubAccounts::get_main_account(buyer)?;
// Retrieve the order
let mut target_order = AllOrders::<T>::get(id).ok_or(Error::<T>::OrderNotFound)?;
if let Some(buyers) = &target_order.buyers {
ensure!(
!buyers.iter().any(|b| b.buyer == buyer.clone()),
Error::<T>::CanOnlyOneOrder
);
}
ensure!(buyer != target_order.seller, Error::<T>::YouAreTheSeller);
ensure!(
target_order.available_amount >= requested_amount,
Error::<T>::NotEnoughAvailableAmount
);
// Ensure the order was created less than an hour ago
ensure!(
<frame_system::Pallet<T>>::block_number() < target_order.valid_until,
Error::<T>::OrderExpired
);
let current_time = T::Time::now().saturated_into::<u64>();
// Update the order
target_order.available_amount -= requested_amount.clone();
let new_buyer = Buyer {
buyer: buyer.clone(),
status: OrderStatus::NoBuyer,
amount: requested_amount.clone(),
time: current_time,
time_to_confirm: <frame_system::Pallet<T>>::block_number() +
T::OrderDuration::get().into(),
};
if let Some(buyers) = &mut target_order.buyers {
buyers.push(new_buyer);
} else {
target_order.buyers = Some(vec![new_buyer]);
}
target_order.last_time_action = T::Time::now().saturated_into::<u64>();
// Update the storage
AllOrders::<T>::insert(id, target_order);
OrdersByUser::<T>::mutate(&buyer, |orders_ids| orders_ids.push(id));
Ok(())
}
/// The buyer can mark the transfer as made.
///
/// The order should be in progress.
/// The seller has 'OrderDuration' blocks to confirm the transfer.
///
/// Parameters:
/// - `id`: order id.
#[pallet::call_index(3)]
pub fn mark_transfer_made(origin: OriginFor<T>, id: u32) -> DispatchResult {
let mut buyer = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
buyer = T::SubAccounts::get_main_account(buyer)?;
let mut target_order = AllOrders::<T>::get(id).ok_or(Error::<T>::OrderNotFound)?;
let current_time = T::Time::now().saturated_into::<u64>();
ensure!(
<frame_system::Pallet<T>>::block_number() < target_order.valid_until,
Error::<T>::OrderExpired
);
// We add reputation only if the buyer made the transfer
Reputation::<T>::mutate(&target_order.seller, |rep| {
if let Some((made, _done)) = rep.as_mut() {
*made += 1;
} else {
*rep = Some((1, 0));
}
});
if let Some(buyers) = &mut target_order.buyers {
if let Some(b) = buyers.iter_mut().find(|b| b.buyer == buyer.clone()) {
b.status = OrderStatus::TransferMade;
b.time = current_time; // Update the time
b.time_to_confirm =
<frame_system::Pallet<T>>::block_number() + T::OrderDuration::get().into();
AllOrders::<T>::insert(id, target_order);
Ok(())
} else {
return Err(Error::<T>::BuyerNotFound.into());
}
} else {
return Err(Error::<T>::NoBuyers.into());
}
}
/// The seller can confirm that he received the funds from the account that set an order.
///
/// Only the seller can call this function.
/// It transfers the cryptos to the buyer.
///
/// Parameters:
/// - `id`: order id.
/// - `buyer_address` : address of the user that mark as made the transfer.
#[pallet::call_index(4)]
pub fn confirm_transfer_received(
origin: OriginFor<T>,
id: u32,
buyer_address: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;
let mut target_order = AllOrders::<T>::get(id).ok_or(Error::<T>::OrderNotFound)?;
// Check if the caller is the seller of the order
ensure!(who == target_order.seller, Error::<T>::NotSeller);
let p2p_id = Self::account_id();
if let Some(buyers) = &mut target_order.buyers {
if let Some(b) = buyers.iter_mut().find(|b| b.buyer == buyer_address.clone()) {
ensure!(b.status == OrderStatus::TransferMade, Error::<T>::TransferNotMarked);
// Ensure less than an hour has passed since the transfer was marked as made
ensure!(
<frame_system::Pallet<T>>::block_number() < b.time_to_confirm,
Error::<T>::TimeLimitExceeded
);
b.status = OrderStatus::TransferReceived;
T::Fungibles::transfer(
target_order.token_id.clone(),
&p2p_id.clone(),
&buyer_address.clone(),
b.amount,
Expendable
)?; // Transfer funds to the buyer
let seller = target_order.seller.clone();
AllOrders::<T>::insert(id, target_order);
Reputation::<T>::mutate(seller, |rep| {
if let Some((_made, done)) = rep.as_mut() {
*done += 1;
}
});
Ok(())
} else {
return Err(Error::<T>::BuyerNotFound.into());
}
} else {
return Err(Error::<T>::NoBuyers.into());
}
}
/// The seller can confirm that he has not received the funds from a buyer.
///
/// Only the seller can call this function.
///
/// Parameters:
/// - `id`: order id.
/// - `buyer_address` : address of the user that mark as made the transfer.
#[pallet::call_index(5)]
pub fn transfer_not_received(
origin: OriginFor<T>,
id: u32,
buyer_address: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;
let mut target_order = AllOrders::<T>::get(id).ok_or(Error::<T>::OrderNotFound)?;
// Check if the caller is the seller of the order
ensure!(who == target_order.seller, Error::<T>::NotSeller);
if let Some(buyers) = &mut target_order.buyers {
if let Some(b) = buyers.iter_mut().find(|b| b.buyer == buyer_address.clone()) {
ensure!(b.status == OrderStatus::TransferMade, Error::<T>::TransferNotMarked);
// Ensure less than an hour has passed since the transfer was marked as made
ensure!(
<frame_system::Pallet<T>>::block_number() < b.time_to_confirm,
Error::<T>::TimeLimitExceeded
);
b.status = OrderStatus::TransferNotReceived;
AllOrders::<T>::insert(id, target_order);
Ok(())
} else {
return Err(Error::<T>::BuyerNotFound.into());
}
} else {
return Err(Error::<T>::NoBuyers.into());
}
}
/// The origin can reclaim its funds wether the user that set a transaction did not send it
/// / or if he did not receive the funds / or if remaining amount was not asked.
/// Then it transfers automatically the funds.
///
/// The origin must be Signed.
///
/// Parameters:
/// - `id`: order id.
#[pallet::call_index(6)]
pub fn reclaim_order_funds(origin: OriginFor<T>, id: u32) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;
let target_order = AllOrders::<T>::get(id).ok_or(Error::<T>::OrderNotFound)?;
// The caller should be the seller
ensure!(who == target_order.seller, Error::<T>::NotSeller);
let p2p_id = Self::account_id();
// After 2 hours, tokens can be back
ensure!(
<frame_system::Pallet<T>>::block_number() >
target_order.valid_until + T::OrderDuration::get().into(),
Error::<T>::ReclaimTimeNotMet
);
// Get the amount of tokens that hasn't been allocated yet
let mut total_reclaimed = target_order.available_amount;
// Get back tokens if status is TransferMade or TransferNotReceived
if let Some(buyers) = &target_order.buyers {
for b in buyers.iter() {
if
b.status == OrderStatus::TransferMade ||
b.status == OrderStatus::TransferNotReceived
{
total_reclaimed += b.amount;
}
}
}
T::Fungibles::transfer(
target_order.token_id,
&p2p_id.clone(),
&who.clone(),
total_reclaimed,
Expendable
)?;
// TODO: Clean storages after the order is closed and reclaimed
// what happens if the total_reclaimed is 0? How can we delete the storages? (This extrinsic will fail)
Ok(())
}
/// The seller can edit the channel and instructions that the buyers should follow.
///
/// Only the seller can call this function.
///
/// Parameters:
/// - `order_id`: order id.
/// - `new_channel`: new channel.
/// - `new_instructions`: new instructions.
#[pallet::call_index(7)]
pub fn modify_order_details(
origin: OriginFor<T>,
order_id: u32,
new_channel: BoundedVec<u8, T::MaxMessageLength>,
new_instructions: BoundedVec<u8, T::MaxMessageLength>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;
//Check if the order exists
let mut order = AllOrders::<T>::get(order_id).ok_or(Error::<T>::OrderNotFound)?;
ensure!(
<frame_system::Pallet<T>>::block_number() < order.valid_until,
Error::<T>::TimeFinished
);
// The caller should be the seller
ensure!(who == order.seller, Error::<T>::NotSeller);
// Update the order
order.channel = new_channel;
order.instructions = new_instructions;
// Store it onchain
AllOrders::<T>::insert(order_id, order);
Ok(())
}
/// The origin can reduce the amout of tokens that he is selling.
///
/// Only the seller can call this function.
///
/// Parameters:
/// - `order_id`: order id.
/// - `reduce_amount`: amount to reduce.
#[pallet::call_index(8)]
pub fn remove_order_amount(
origin: OriginFor<T>,
order_id: u32,
reduce_amount: BalanceOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin use the main account to store data
who = T::SubAccounts::get_main_account(who)?;
//Check if the order exists
let mut order = AllOrders::<T>::get(order_id).ok_or(Error::<T>::OrderNotFound)?;
// The caller should be the seller
ensure!(who == order.seller, Error::<T>::NotSeller);
ensure!(
OrderUserTokenId::<T>::contains_key((&who, order.token_id.clone())),
Error::<T>::OrderNotExists
);
ensure!(
<frame_system::Pallet<T>>::block_number() < order.valid_until,
Error::<T>::TimeFinished
);
// The new amount should not be less than the available amount
ensure!(reduce_amount <= order.available_amount, Error::<T>::NotEnoughAvailableAmount);
// Update the amount
order.amount = order.amount.saturating_sub(reduce_amount);
order.available_amount = order.available_amount.saturating_sub(reduce_amount);
order.created_at = T::Time::now().saturated_into::<u64>();
let p2p_id = Self::account_id(); // Get the account/id of the p2p pallet
T::Fungibles::transfer(
order.token_id,
&p2p_id.clone(),
&who.clone(),
reduce_amount,
Expendable
)?; // Transfer funds from the p2p account
// Store it onchain
AllOrders::<T>::insert(order_id, &order);
OrderUserTokenId::<T>::insert((&who, &order.token_id), &order);
Ok(())
}
}
impl<T: Config> Pallet<T> {
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
}
}