#[starknet::interface]
pub trait ITokenSender<TContractState> {
/// Multisend function
/// # Arguments
/// - `token_address` - The address of the token contract
/// - `transfer_list` - The list of transfers to perform
fn multisend(
self: @TContractState, token_address: ContractAddress, transfer_list: Array<TransferRequest>
) -> ();
}
#[starknet::contract]
pub mod TokenSender {
use starknet::{get_caller_address, ContractAddress, get_contract_address};
use crate::erc20::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use super::TransferRequest;
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
TokensSent: TokensSent,
}
#[derive(Drop, starknet::Event)]
struct TokensSent {
token_address: ContractAddress,
recipients: felt252,
}
#[constructor]
fn constructor(ref self: ContractState,) {}
#[storage]
struct Storage {}
#[abi(embed_v0)]
impl TokenSender of super::ITokenSender<ContractState> {
fn multisend(
self: @ContractState,
token_address: ContractAddress,
transfer_list: Array<TransferRequest>
) {
let erc20 = IERC20Dispatcher { contract_address: token_address };
let mut total_amount: u256 = 0;
for t in transfer_list.span() {
total_amount += *t.amount;
};
erc20.transfer_from(get_caller_address(), get_contract_address(), total_amount);
for t in transfer_list.span() {
erc20.transfer(*t.recipient, *t.amount);
};
}
}
}
```
Scarb toml example
This is and example of a modern Scarb toml. It uses openzeppelin libraries from the scarbs.xyz registry, 0.20.0 is the latest version
```toml
[package]
name = "token_sender"
version = "0.5.0"
license-file = "LICENSE"
edition = "2024_07"
Importing a component
Importing a component requires a few rules
Adding the component with the component macro
Example:
```cairo
component!(path: ERC20Component, storage: erc20, event: ERC20Event);
```
Adding the storage to the contract storage
Example:
```cairo
#[storage]
struct Storage {
#[substorage(v0)]
erc20: ERC20Component::Storage
}
```
Adding the events to the contract events:
```cairo
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
ERC20Event: ERC20Component::Event
}
```
If there are internal implementation in the component, they need to be seperatly imported as Impl
Example:
```cairo
#[abi(embed_v0)]
impl ERC20MixinImpl = ERC20Component::ERC20MixinImpl<ContractState>;
impl ERC20InternalImpl = ERC20Component::InternalImpl<ContractState>;
```
Full example of adding an ERC20 component from OpenZeppelin (OZ)
```cairo
#[starknet::contract]
mod MyERC20Token {
use openzeppelin_token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::ContractAddress;
This is an example of a lib.cairo file, it is a mandatory file in src lib. It specified the modules that are available in the library. In this case there is erc20 module and a token sender module, as with the implementation seen above.
```cairo
pub mod erc20;
pub mod token_sender;
```
Implementation of interface
Component exampleImplementations of the interface start with the attribute `
```cairo
#[abi(embed_v0)]
```
Testing and Deployment:
Develop comprehensive unit and integration tests
Use Starknet's testing frameworks, Starknet-Foundry
Simulate on-chain environments
Perform thorough testnet validation before mainnet deployment
Implement CI/CD pipelines for automated testing
Tests are usually added in the `./tests` directory of the project, and are also cairo files
Tests are annotated with the `#[test]` annotation
Tests use the `snforge_std` library and its cheatcodes to perform tests. The test file needs to import the module it is testing
Example:
```cairo
use snforge_std::{declare, cheat_caller_address, ContractClassTrait, CheatSpan, DeclareResultTrait};
use snforge_std::trace::get_call_trace;
use starknet::{contract_address_const, ContractAddress};
use token_sender::erc20::erc20::{IERC20Dispatcher, IERC20DispatcherTrait};
use token_sender::token_sender::{ITokenSenderDispatcher, ITokenSenderDispatcherTrait};
use token_sender::token_sender::TransferRequest;
```
And example of a setup function, delcaring and deploying and ERC20 contract, and a TokenSender contract
```cairo
fn setup() -> (ContractAddress, ContractAddress) {
let erc20_class_hash = declare("MockERC20").unwrap().contract_class();
// let account: ContractAddress = get_contract_address();
let account: ContractAddress = contract_address_const::<1>();
// let account: ContractAddress = get_contract_address();
let mut calldata = ArrayTrait::new();
INITIAL_SUPPLY.serialize(ref calldata);
account.serialize(ref calldata);
let (erc20_address, _) = erc20_class_hash.deploy(@calldata).unwrap();
let token_sender_class_hash = declare("TokenSender").unwrap().contract_class();
// let account: ContractAddress = get_contract_address();
let mut calldata = ArrayTrait::new();
let (token_sender_address, _) = token_sender_class_hash.deploy(@calldata).unwrap();
(erc20_address, token_sender_address)
}
```
Here is an example of a test using the return values of the setup function above, to test the functionality of the TokenSender contract
```cairo
#[test]
fn test_multisend() {
let (erc20_address, token_sender_address) = setup();
let erc20 = IERC20Dispatcher { contract_address: erc20_address };
let account: ContractAddress = contract_address_const::<1>();
assert(erc20.balance_of(account) == INITIAL_SUPPLY, 'Balance should be > 0');
cheat_caller_address(erc20_address, account, CheatSpan::TargetCalls(1));
let transfer_value: u256 = 100;
erc20.approve(token_sender_address, transfer_value * 2 - 1);
assert(
erc20.allowance(account, token_sender_address) == transfer_value * 2,
'Allowance not
set',
);
let balance = erc20.balance_of(account);
println!("Balance {}", balance);
// Send tokens via multisend
let token_sender = ITokenSenderDispatcher { contract_address: token_sender_address };
let dest1: ContractAddress = contract_address_const::<2>();
let dest2: ContractAddress = contract_address_const::<3>();
let request1 = TransferRequest { recipient: dest1, amount: transfer_value };
let request2 = TransferRequest { recipient: dest2, amount: transfer_value };
let mut transfer_list = ArrayTrait::<TransferRequest>::new();
transfer_list.append(request1);
transfer_list.append(request2);
// need to also cheat the token sender
cheat_caller_address(token_sender_address, account, CheatSpan::TargetCalls(1));
token_sender.multisend(erc20_address, transfer_list);
let balance_after = erc20.balance_of(dest1);
assert(balance_after == transfer_value, 'Balance should be > 0');
let balance_after = erc20.balance_of(dest2);
assert(balance_after == transfer_value, 'Balance should be > 0');