Hello everyone..
Welcome to another day of exploring Web3 Engineering. In today's blog, let us check on how to make payments using coin
module on Sui.
We all know about the ERC20
standard in the Ethereum and how it is different from the native eth
payments in the solidity contracts. Having 2 different payment currencies poses a lot of problems and excessive work in the solidity ecosystem. To avoid that case, Sui contains the Coin
standard and the native SUI currency is also implemented using the same standard. Which means we don't have to write different functions to handle payments of SUI and other tokens.
Below we have a counter contract that accepts the SUI Coin payments.
/// Module: coin_payments
module coin_payments::coin_payments {
use sui::sui::SUI;
use sui::coin::{Self, Coin};
use sui::balance::{Self, Balance};
public struct CoinPayments has key {
id: UID,
new_fee: u64,
update_fee: u64,
balance: Balance<SUI>
}
public struct CoinPaymentsCap has key {
id: UID
}
public struct Counter has key,store{
id: UID,
val: u64
}
// Errors
const ENegativeDecrement: u64 = 0;
const EInvalidPayment: u64 = 1;
fun init(ctx: &mut TxContext) {
transfer::transfer(CoinPaymentsCap {
id: object::new(ctx)
}, ctx.sender());
transfer::share_object(CoinPayments {
id: object::new(ctx),
new_fee: 1000,
update_fee: 100,
balance: balance::zero<SUI>(),
})
}
#[allow(lint(self_transfer))]
public fun new(cp: &mut CoinPayments, payment: Coin<SUI>, ctx: &mut TxContext) {
assert!(cp.new_fee == payment.value(), EInvalidPayment);
coin::put(&mut cp.balance, payment);
transfer::transfer(Counter {
id: object::new(ctx),
val:0
}, ctx.sender())
}
public fun increment(c: &mut Counter, cp: &mut CoinPayments, payment: Coin<SUI>) {
assert!(cp.update_fee == payment.value(), EInvalidPayment);
coin::put(&mut cp.balance, payment);
c.val = c.val + 1
}
public fun decrement(c: &mut Counter, cp: &mut CoinPayments, payment: Coin<SUI>) {
assert!(c.val > 0, ENegativeDecrement);
assert!(cp.update_fee == payment.value(), EInvalidPayment);
coin::put(&mut cp.balance, payment);
c.val = c.val - 1
}
public fun val(c: &Counter) :u64 {
c.val
}
public entry fun withdraw(cp: &mut CoinPayments, _: &CoinPaymentsCap, ctx: &mut TxContext) {
let val = cp.balance.value();
let payment = coin::take(&mut cp.balance, val, ctx);
transfer::public_transfer(payment, ctx.sender())
}
#[test_only]
public fun init_for_testing(ctx: &mut TxContext) {
init(ctx);
}
}
Here we can see that Sui payments are payments are received by receiving Coin<SUI>
as an argument with the required value. Unlike the Solidity msg.value
as a transaction parameter, we are directly including it as a function argument.
And here if we have a close look, we are using 2 objects and they are Coin
and Balance
. Coin is the transferable object of the given currency while the Balance
is the stored value of it. Here, we are storing the payment received as Balance in the CoinPayments
object but transferring it to the user as Coin
object.
Now, let us look into the test cases for the above contract.
#[test_only]
module coin_payments::coin_payments_tests {
use sui::coin;
use sui::sui::SUI;
use sui::test_scenario;
use coin_payments::coin_payments;
const USER: address = @0xFACE;
const ADMIN: address = @0xCAFE;
#[test]
fun test_coin_payments() {
let mut scenario = test_scenario::begin(ADMIN);
{
coin_payments::init_for_testing(scenario.ctx());
};
scenario.next_tx(USER);
{
let mut cp = test_scenario::take_shared<coin_payments::CoinPayments>(&scenario);
let c = coin::mint_for_testing<SUI>(1000, scenario.ctx());
coin_payments::new(&mut cp, c, scenario.ctx());
test_scenario::return_shared(cp);
};
scenario.next_tx(USER);
{
let mut cp = test_scenario::take_shared<coin_payments::CoinPayments>(&scenario);
let c = coin::mint_for_testing<SUI>(100, scenario.ctx());
let mut counter = scenario.take_from_sender<coin_payments::Counter>();
coin_payments::increment(&mut counter, &mut cp, c);
scenario.return_to_sender(counter);
test_scenario::return_shared(cp);
};
scenario.next_tx(ADMIN);
{
let mut cp = test_scenario::take_shared<coin_payments::CoinPayments>(&scenario);
let cap = scenario.take_from_sender<coin_payments::CoinPaymentsCap>();
coin_payments::withdraw(&mut cp, &cap, scenario.ctx());
scenario.return_to_sender(cap);
test_scenario::return_shared(cp);
};
scenario.end();
}
}
Here in the above test cases, we are testing both paying as a user scenario and also withdrawing as the admin scenario.