Contribute to this guide in the ./docs folder of the repo
1. Introduction
What about when we want to use XLM inside a Soroban smart contract? How do we trigger those transactions? Can we trigger transactions on behalf the user using the require_auth method?
In this chapter we will write a smart contract that will interact with our XML balance!
2. A donations contract
In order to interact with our XML balance inside a Soroban smart contract, let's write a "donations" contract where any donor can send XML to the contract, and a "recipient" can then withdraw all the funds:
Check the code: All the code used in this chapter is available in https://github.com/esteblock/donations-dapp-soroban
Our contract will have this functions:
// Contract TraitpubtraitDonationsTrait {// Sets the recepient address and the token that will be accepted as donationfninitialize(e:Env, recipient:Address, token:Address);// Donates amount units of the accepted tokenfndonate(e:Env, donor:Address, amount:i128);// Transfer all the accumulated donations to the recipient. Can be called by anyonefnwithdraw(e:Env);// Get the token address that is accepted as donationsfntoken(e:Env) ->Address;// Get the donations recipient addressfnrecipient(e:Env) ->Address;}
The full code will be:
#![no_std]use soroban_sdk::{ contract, contractimpl, Env, Address, Val, TryFromVal, ConversionError, token};mod test;#[derive(Clone, Copy)]// Data KeyspubenumDataKey {AcceptedToken=0, // address of the accepted tokenDonationsRecipient=1, // address of the donations recipient}implTryFromVal<Env, DataKey> forVal {typeError=ConversionError;fntry_from_val(_env:&Env, v:&DataKey) ->Result<Self, Self::Error> {Ok((*v asu32).into()) }}// Helper functionsfnput_token_address(e:&Env, token:&Address) { e.storage().instance().set(&DataKey::AcceptedToken, token);}fnput_donations_recipient(e:&Env, recipient:&Address) { e.storage().instance().set(&DataKey::DonationsRecipient, recipient);}fnget_token_address(e:&Env) ->Address { e.storage().instance().get(&DataKey::AcceptedToken).expect("not initialized")}fnget_donations_recipient(e:&Env) ->Address { e.storage().instance().get(&DataKey::DonationsRecipient).expect("not initialized")}fnget_balance(e:&Env, token_address:&Address) ->i128 {let client = token::Client::new(e, token_address); client.balance(&e.current_contract_address())}// Transfer tokens from the contract to the recipientfntransfer(e:&Env, to:&Address, amount:&i128) {let token_contract_address=&get_token_address(e);let client = token::Client::new(e, token_contract_address); client.transfer(&e.current_contract_address(), to, amount);}// Contract TraitpubtraitDonationsTrait {// Sets the recepient address and the token that will be accepted as donationfninitialize(e:Env, recipient:Address, token:Address);// Donates amount units of the accepted tokenfndonate(e:Env, donor:Address, amount:i128);// Transfer all the accumulated donations to the recipient. Can be called by anyonefnwithdraw(e:Env);// Get the token address that is accepted as donationsfntoken(e:Env) ->Address;// Get the donations recipient addressfnrecipient(e:Env) ->Address;}#[contract]structDonations;// Contract implementation#[contractimpl]implDonationsTraitforDonations {// Sets the recepient address and the token that will be accepted as donationfninitialize(e:Env, recipient:Address, token:Address){assert!(!e.storage().instance().has(&DataKey::AcceptedToken),"already initialized" );put_token_address(&e, &token);put_donations_recipient(&e, &recipient); }// Donor donates amount units of the accepted tokenfndonate(e:Env, donor:Address, amount:i128){ donor.require_auth();//assert!(amount > 0, "amount must be positive");let token_address =get_token_address(&e);let client = token::Client::new(&e, &token_address); // Transfer from user to this contract client.transfer(&donor, &e.current_contract_address(), &amount); }// Transfer all the accumulated donations to the recipient. Can be called by anyonefnwithdraw(e:Env){let token =get_token_address(&e);let recipient =get_donations_recipient(&e);transfer(&e, &recipient, &get_balance(&e, &token)); }// Get the token address that is accepted as donationsfntoken(e:Env) ->Address{get_token_address(&e) }// Get the donations recipient addressfnrecipient(e:Env) ->Address{get_donations_recipient(&e) }}
2. Testing the contract with rs-soroban-sdk
The first thing we allways do when we write an smart contract is to test it inside a test.rs file and we test it with make test. This will test the contract in a Soroban environment provided by rs-soroban-sdk. (the rust soroban-sdk)
How can we tell the contract that we want to use the native XML? Well.... I an not pretty sure... and this is why I am opening this discussion on Discord (here)[https://discord.com/channels/897514728459468821/1145462925109231726/1145462925109231726]
In the test.rs file you'll find 2 tests. One is for any type of tokens, and works perfect. The second test is ment to be only for the native XML token....
When you create the XML native token inside test.rs you get:
fnnative_asset_contract_address(e:&Env) ->Address {let native_asset =Asset::Native;let contract_id_preimage =ContractIdPreimage::Asset(native_asset);let bytes =Bytes::from_slice(&e, &contract_id_preimage.to_xdr().unwrap());let native_asset_address =Address::from_contract_id(&e.crypto().sha256(&bytes)); native_asset_address}// Set the native token addresslet native_address =native_asset_contract_address(&e); let expected_address_string ="CDF3YSDVBXV3QU2QSOZ55L4IVR7UZ74HIJKXNJMN4K5MOVFM3NDBNMLY";letStrkey::Contract(array) =Strkey::from_string(expected_address_string).unwrap() else { panic!("Failed to convert address") };let contract_id =BytesN::from_array(&e, &array.0);let expected_asset_address =Address::from_contract_id(&contract_id);assert_eq!(native_address, expected_asset_address);
Until there everything is OK, but if you'll later want to check any user's balance.... how van we do it inside test.rs? I get these errors:
3. Testing the contract inside a Quickstart Standalone blockchain and soroban-cli
Because tests in soroban-sdk did not work very well, we'll then deploy the contract in a real quickstart blockchain and we'll tell the contract that we'll use the nattive token address used in the previous chapers.
Check the code in: https://github.com/esteblock/donations-dapp-soroban/blob/main/test_soroban_cli.sh
Also, when checking for accounts balances, in order to be sure, we'll check the balance both using the native token contract address and the classic way with node and the js-soroban-sdk (the javascript soroban-sdk)
var server =newStellarSdk.Server(horizonUrl, {allowHttp:true});server.loadAccount(address).then(account => {// Find the XLM balanceconstxlmBalance=account.balances.find(balance =>balance.asset_type ==='native');console.log(`Address: ${address} | XLM balance: ${xlmBalance.balance}`); }).catch(error => {console.error('Error loading account:', error); });
If you wanna do the whole tests, just follow the instructions in the repo! You should get:
Why is the first donation failing? I don't know yet. Opened Discussion in Discord (here)[https://discord.com/channels/897514728459468821/1145688416432963705/1145688416432963705]
This Playground chapter has been written by @esteblock