A Charitable Solana Token Launch with The Giving Block

July 16 2022


Introduction


In this post we are going to create a launch Dapp for a pair of custom tokens on the Solana blockchain, which incorporates charitable giving as a core part of the payment process.

We will be using a 'pay what you want' model similar to something like Humble Bundle, in which participants will be able to pay whatever they want for a block of 1000 tokens above some small minimum price. In our implementation we have set that minimum to 0.0001 SOL which is about 0.35 US cents at time of writing. They will also be able to choose how much of that payment we donate to their choice of charity, and how much stays with us, the app's developers.

Additionally, if a user pays more than the current average they will not only get double the tokens, but they will also receive a special supporters token, which might be used as a governance token for your project, or to unlock special features in your applications.

You can find the complete source code for the on-chain program and a rust based client here, and for a simple javascript front-end interface here. The front-end application can be tested at the bottom of this post, where it displays the current headline stats, and will allow you to join the token launch. Note this is running on the Solana devnet network so there are no real donations being made!

In order to make the charitable components of the launch as transparent and streamlined as possible, we will be using The Giving Block (TGB) to handle all the donations. TGB are partnered with over a thousand non-profit organizations spread across a huge range of different sectors, and from those we have selected a short list of seven that can be chosen by participants of our token launch.

To summarize, by the end of the post we will have completed the following:


Creating Solana Tokens


To create our tokens on the Solana blockchain we will be making use of the Strata Protocol launchpad. Strata makes creating new tokens extremely straight forward, with the whole process taking only a matter of minutes. They also charge zero additional fees to use their platform, which means creating your token only costs about 0.01 SOL, or 30 US cents at time of writing.

You can specify which network you want to launch your token on from the drop down menu on the top right. For this post we will be creating two tokens on the devnet network. The first will be the primary token for our application, which we will imaginatively call the 'Dao Plays Test Token', with symbol DPTT. We want this to feel like the sort of token you would get in an old fashioned fairground or arcade, so we will set decimals to zero. Our launch Dapp will be selling blocks of 1000 or 2000 tokens so we set a supply of 100 million for an optimistic cap of about fifty to one hundred thousand participants.

The launchpad then provides two final options: i) whether to keep the mint authority, which will allow you to mint more of this sort of token in the future, and ii) whether to keep the freeze authority, which will allow you to freeze token accounts associated with this token. Neither of our tokens will keep their freeze authority, and for this first token type we will also not keep the mint authority, so our supply of 100 million tokens will be all that is ever created. When you click the Create Token button you will be prompted to authorize a couple of transactions related to setting up the accounts that will hold your tokens and the token meta data, and then you are done!

We then repeat this process for our second token, which we call the Dao Plays Supporter Test Token. In this case we keep decimals as zero, but as we will initially only need a much smaller quantity we set the supply to be one hundred thousand. Given we anticipate that this token will be used over many applications going forward, rather than just one as with the primary token type, we also retain mint authority so that we can produce more if that becomes necessary.

You can view your newly created tokens on explorer.solana by entering the mint address that was reported at the end of the creation process. For example, our two tokens can be seen here and here (also pictured below).



token image
supporter image

Creating Donation Wallets With The Giving Block


Now that we have our tokens it is time to set up our charity wallets. The Giving Block allow you to donate either to individual organizations, or what they refer to as index funds, which makes it easy to contribute to a collection of similarly themed organizations, where the donation is split evenly between all the members of the index.

In our example we will be including one index fund (the Ukraine Emergency Response Fund), and six individual charities, which we list below. These cover a range of different sectors such as the environment, accessibility to water, education, human rights, health & medicine, and effective giving. You can see a short description of each of these charities in their own words by hovering over the logos below, and can find out more by following the links to their TGB page. We also include a description of each in our front end application at the end of the post.



Ukraine ERF
Water.Org
One Tree Planted
Evidence Action
Girls Who Code
Outreach International
The Life you can Save


When you visit an organization's page on TGB you can use the widget to select the cryptocurrency you want your wallet to be in, and can then choose to either enter your personal details, or donate anonymously. At this point you are given a unique public key that is tied to that organization, and any donations made to that account will be tied to you (if you entered your details). This widget makes use of dynamic wallet addresses, so that every time you go back to the page and repeat this process you will be presented with a new address. This means that if privacy is a concern you can donate from different personal wallets, to different TGB wallets, in order to obfuscate your donations from third parties.

The only drawback with this process currently is that there is no way to cryptographically verify that one of these dynamic addresses really is related to The Giving Block, or therefore, to verify which cause it is for. This is a shortcoming that they are currently working to resolve, but until then when developing a trustless DApp, we have to rely on a non-cryptographic solution to publicly verify the accounts. In particular, after getting your wallets it is necessary for you to donate a small amount to each one, publicly display the transactions to these (for example by tweeting the transaction ids), and then TGB will verify via their twitter account that these donations were for genuine accounts, and which causes they are linked to.

While this allows potential users of your program to be confident that the wallets they are sending their crypto to are genuine, it unfortunately doesn't solve the reverse problem. It would be great to have the option for a user of our token sale to simply provide us with the account of a charity they would like to support, rather than having to choose one from a list. However currently there is no way for us to verify on chain in an automated way that the account really is a TGB account, hence why we must provide a list of authenticated accounts. We will be sure to post an updated version of this guide when the cryptographic solutions are available.

Creating The Launch Program


Now that we have both our tokens and our charity wallets we can start putting together the program that is going to actually handle the token launch. Our program will have 3 main functions:



You can find the full code for this program at our Github repo, here.

Initialising The Token Launch


The first of the three functions introduces almost all of the key Solana concepts that will be at play within the program as a whole, namely 'program derived addresses', 'associated token accounts', and 'cross program invocation'. We will go through each of these in turn below as we implement the three main tasks that this function must perform:




Creating The Program's Data Account


The most straight forward way to handle a token launch is to have our program be responsible for sending out tokens to participants. Right now, however, the tokens are sitting in our own accounts, and the program doesn't have any authority to take the tokens from there, and send them on as needed. We therefore need to give the program control over the tokens, and to do that we first create (or more accurately, find) what is referred to as a 'Program derived address' (PDA).

There are many in depth discussions of exactly what PDAs are already available online (for example here or here) so we won't spend much time discussing the technical details of how they differ from a standard wallet address. For our purposes it is enough to say that a PDA is nothing more than the public key for an account that is owned by a program rather than by a person. Having a PDA will allow the program to have it's own token accounts, and critically will allow it to sign transactions that involve sending tokens from those accounts. Only the program that owns the PDA will be able to sign such transactions, and so in this regard the PDA provides the same functionality to the program as a keypair provides to a human.

The Solana API provides the find_program_address function to find a PDA for your program which can be used on or off chain to easily obtain the right public key, without having to actually store it anywhere. This function takes as arguments an array of bytes that will be used as a seed (in our case just the string "token_account", where the lower case "b" converts that to a byte array), and the program's own public key.



let (expected_pda, bump_seed) = Pubkey::find_program_address(
&[b"token_account"],
program_id);

If you look at the source for this function, you can see that all it is doing is calling the create_program_address function using both the provided seed, and a separate bump seed which starts at a value of 255 and decreases by one until the function finds a valid PDA. Once it has been found it is recommended to directly call create_program_address on chain and simply pass the correct bump_seed, rather than reusing find_program_address as the former can be significantly less costly.

Now that we know where the program's account is going to live we need to actually create it, and for this we make use of Solana's create_account function:

pub fn create_account(
from_pubkey: &Pubkey,
to_pubkey: &Pubkey,
lamports: u64,
space: u64,
owner: &Pubkey
) -> Instruction

The from_pubkey refers to the wallet address that will fund the creation of the account, and the to_pubkey is simply the address where we want to create our new account which will be the PDA. The third pubkey that is passed as the last argument, owner, is the address of our program. In english then, this will create a new account for which our program will be the owner, at the location of our newly found PDA, and we will fund it's creation with our own wallet.

The other two arguments, lamports and space are related. In order to have an account and to store data in that account you need to pay what is referred to as rent. There are two supported methods of paying rent, but we will only be interested in the rent-exempt method, where we deposit enough lamports into the account to ensure it survives indefinitely without being deleted.

As you might expect, the more data you want to store the more rent you need to pay. If you have the solana CLI installed you can use this to check the current rent-exempt cost for any amount of data. For example, if you ask for the rent required for 0 bytes, you will be shown the cost to have a basic account that stores no additional data:

$solana rent 0
Rent per byte-year: 0.00000348 SOL
Rent per epoch: 0.000002439 SOL
Rent-exempt minimum: 0.00089088 SOL

In order to achieve this rent exempt state we will therefore have to know what we want to store, and what the rent-exemption cost will be to store it. We define the following struct in our program which is going to save a summary of the totals donated to each charity, the total paid, and the number of accounts that have participated in the launch.

// in state.rs
pub struct ICOData {
pub charity_totals : [u64 ; 7],
pub donated_total : u64,
pub paid_total : u64,
pub n_donations : u64
}

As this is quite a simple struct we could quite easily work out the size by hand, and then check the rent-exempt cost. The borsh library however provides a straight forward way to do this for us using the try_to_vec function.

pub fn get_state_size() -> usize
{
let encoded = ICOData
{
charity_totals: [0; 7],
donated_total : 0,
paid_total : 0,
n_donations : 0
}.try_to_vec().unwrap();
encoded.len()
}

The minimum_balance function provided by Solana then takes this size and returns the current balance in lamports required for us to keep the account rent free. Our function to create the program's data account that combines these elements is shown below:

// in processor.rs
// create the program's data account
fn create_program_account<'a>(
// the wallet that will be paying to create the token account
funding_account: &AccountInfo<'a>,
// the program account that we want to create
pda : &AccountInfo<'a>,
// the address of the program
program_id : &Pubkey,
bump_seed : u8
) -> ProgramResult
{
let data_size = get_state_size();
let space : u64 = data_size.try_into().unwrap();
let lamports = rent::Rent::default().minimum_balance(data_size);
msg!("Require {} lamports for {} size data", lamports, data_size);
let ix = solana_program::system_instruction::create_account(
funding_account.key,
pda.key,
lamports,
space,
program_id,
);
...

In order to actually execute the instruction we need to make use of cross program invocation (see here for more details) which simply means that we are going to call a different program from within our program. In this case we are going to be calling the system program which is responsible for creating new accounts, and we do so with the invoke_signed function below.

...
// Sign and submit transaction
invoke_signed(
&ix,
&[funding_account.clone(), pda.clone()],
&[&[b"token_account", &[bump_seed]]]
)?;
Ok(())
}

The seeds that are passed are the same as those that we used to find our derived program address initially, along with the bump seed returned by that program. In this way the program is able to sign the transaction that will create it's own account.

Creating The Token Accounts


The next piece of functionality we need to add is the ability to create the token accounts for our program. On the Solana blockchain each user can only store tokens of a particular type in a token account that corresponds to that type of token. As discussed in the Solana docs it is perfectly possible for a user to have multiple token accounts for the same token, which can make it difficult for a program to know which account tokens should be sent to. The Associated Token Program was thus introduced as a way to deterministically derive a token account address from the combination of a user's wallet address and the token's mint address. As both the user and the program will arrive at the same token account address when using this method it also makes it easy for the program to create the account when required and overall reduces friction during token launches.

These accounts are called Associated Token Accounts, and in order to create one we first need to get the address using the get_associated_token_address function. This takes either the wallet address of a user, or in this case, the address of our program's account, and the mint address of our token:

// in spl_associated_token_account
pub fn get_associated_token_address(
wallet_address: &Pubkey,
spl_token_mint_address: &Pubkey
) -> Pubkey

Once we have this address we can then simply call create_associated_token_address to actually create the account:

// in spl_associated_token_account
pub fn create_associated_token_account(
funding_address: &Pubkey,
wallet_address: &Pubkey,
spl_token_mint_address: &Pubkey
) -> Instruction

With these functions in mind we then have everything we need to implement the creation of our token accounts. We show below out complete function which we will simply call twice when initialising the token launch, once for the main token account, and once for the supporters token account. Note in this case we need only call invoke rather than invoke_signed because it is the funding wallet that will be signing the transaction, rather than our program.

fn create_token_account<'a>(
// the wallet that will be paying to create the token account
funding_account : &AccountInfo<'a>,
// the account that will own the new token account
wallet_account : &AccountInfo<'a>,
// the mint of the token account
token_mint_account : &AccountInfo<'a>,
// the address of the token account, found through get_associated_token_account
new_token_account : &AccountInfo<'a>,
// the spl_token program account
token_program_account : &AccountInfo<'a>
) -> ProgramResult
{
let create_ATA_idx = create_associated_token_account(
&funding_account.key,
&wallet_account.key,
&token_mint_account.key
);
invoke(
&create_ATA_idx,
&[
funding_account.clone(),
new_token_account.clone(),
wallet_account.clone(),
token_mint_account.clone(),
token_program_account.clone()
],
)?;
Ok(())
}

Transferring The Tokens


The final task we must perform in this function is to transfer our tokens to the programs newly created token accounts. To do this we will be making use of the spl_token function transfer:

// in spl_token::instruction
pub fn transfer(
token_program_id: &Pubkey,
source_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
authority_pubkey: &Pubkey,
signer_pubkeys: &[&Pubkey],
amount: u64
) -> Result<Instruction, ProgramError>

The arguments here are relatively self explanatory, the authority_pubkey in this case will be our wallet, however when the program is transferring tokens to other users that will be the PDA. In our use cases we will be leaving the signers vector empty. Our complete function to handle transferring the tokens is shown below, and it's structure should by now by quite familiar! We simply create the instruction, and then call invoke_signed to handle the cross program invocation and actually enact the transaction. Note that in this case we could have used invoke, as our PDA doesn't need to sign this transaction, however when the program is sending out tokens then we do need to use invoke_signed and it is simpler to just use it generically for both cases.

// in processor.rs
fn transfer_tokens<'a>(
// the amount in tokens that will be transferred
amount : u64,
// the token account that will act as the source
token_source_account : &AccountInfo<'a>,
// the token account to send to
token_dest_account : &AccountInfo<'a>,
// the account that will sign the transaction
authority_account : &AccountInfo<'a>,
// the spl_token account
token_program_account : &AccountInfo<'a>,
// the bump_seed from our PDA
bump_seed : u8
) -> ProgramResult
{
let ix = spl_token::instruction::transfer(
token_program_account.key,
token_source_account.key,
token_dest_account.key,
authority_account.key,
&[],
amount,
)?;
invoke_signed(
&ix,
&[
token_source_account.clone(),
token_dest_account.clone(),
authority_account.clone(),
token_program_account.clone()
],
&[&[b"token_account", &[bump_seed]]]
)?;
Ok(())
}

Joining The Token Launch


The second of our main functions will allow users to participate in the token launch, and will be responsible for handling the SOL payments and sending out the different token types to the participants. The overall flow of the code is as follows:



This function expects to be passed thirteen accounts, and as before we will skip over the loading and checking of all of these in this post. The first real check we make is to ensure that the minimum amount to be paid has been exceeded. We do this before creating any accounts or going further into the function.

When calling the join_ico function from off chain we pass a JoinMeta struct, which has the following fields:

// in state.rs
pub struct JoinMeta {
pub amount_charity : u64,
pub amount_dao : u64,
pub charity : Charity
}

Here amount_charity and amount_dao are the amounts in lamports to be paid to the chosen charity, and to the developers respectively. The Charity type of the final member of this struct refers to an enum we have defined in state.rs:

// in state.rs
pub enum Charity {
UkraineERF,
WaterOrg,
OneTreePlanted,
EvidenceAction,
GirlsWhoCode,
OutrightActionInt,
TheLifeYouCanSave
}

In order to check that the minimum amount has been exceeded, we therefore need only sum amount_charity and amount_dao and compare that to the minimum, which in this example we have set to 0.0001 SOL, or about 0.35 US cents at time of writing:

...
// check that this transaction is valid:
// i) total amount should exceed the minimum
// ii) joiner should not already have tokens
// iii) program should have enough spare tokens
msg!("Transfer {} {}", meta.amount_charity, meta.amount_dao);
msg!("Balance {}", joiner_account_info.try_borrow_lamports()?);
// minimum amount is 0.0001 SOL, or 100000 lamports
let min_amount : u64 = 100000;
if meta.amount_charity + meta.amount_dao < min_amount {
msg!("Amount paid is less than the minimum of 0.0001 SOL");
return Err(ProgramError::InvalidArgument);
}
...

We next check if the joiners associated token account already exists by using try_borrow_lamports, which will return a positive value if it has been previously initialised and there are lamports present in it. If it doesn't exist we create it using our create_token_account function:

...
// check if we need to create the joiners token account
if **joiner_token_account_info.try_borrow_lamports()? > 0 {
msg!("Users token account is already initialised.");
}
else {
msg!("creating user's token account");
Self::create_token_account(
joiner_account_info,
joiner_account_info,
token_mint_account_info,
joiner_token_account_info,
token_program_account_info
)?;
}
...

The next step is to check whether the user's token account already has Dao Plays Test Tokens in it. In an ideal world we would be able to limit a person to participating only once in the token launch, so that we could maximise the number of unique users taking part. Unfortunately this is impossible, as a single user can just create multiple accounts in order to participate as many times as they would like. We at least want to make our intentions clear though, and so stop someone from participating if they already have tokens in their account so that they at least need to spend transaction costs to either move them elsewhere, or to create a new token account with a different wallet. In order to do this we need to get the account's data from the token account, which we do with the provided unpack_unchecked function:

...
let program_token_account = spl_token::state::Account::unpack_unchecked(&program_token_account_info.try_borrow_data()?)?;
let program_supporters_token_account = spl_token::state::Account::unpack_unchecked(&program_supporters_token_account_info.try_borrow_data()?)?;
let joiner_token_account = spl_token::state::Account::unpack_unchecked(&joiner_token_account_info.try_borrow_data()?)?;
msg!("token balances: {} {} {}", program_token_account.amount, program_supporters_token_account.amount, joiner_token_account.amount);
if joiner_token_account.amount > 0 {
msg!("Tokens already present in joiners account, thank you for taking part!");
return Err(ProgramError::InvalidAccountData);
}
...

Next we want to check whether the participant has paid more than the current average, in which case they are awarded supporter status, and so will receive double the number of DPTTs, and an additional supporters token. The current average price is stored in the program's data account, so we first have to retrieve that using the try_from_slice function from the borsh crate. As a last check before proceeding we simply make sure there are enough DPTTs remaining. We don't need to do this for the supporters tokens as we have made sure there is a sufficient supply even if every participant is a supporter.

...
// get the data stored in the program account to access current state
let mut current_state = ICOData::try_from_slice(&program_data_account_info.data.borrow()[..])?;
// calculate the current average to see if this individual has paid more
let current_average = current_state.paid_total / current_state.n_donations;
let total_paid = meta.amount_charity + meta.amount_dao;
let mut ico_token_amount : u64 = 1000;
let mut supporter = false;
// if they have then they get double!
if total_paid > current_average {
msg!("Thank you for paying over the average price!");
ico_token_amount = 2000;
supporter = true;
}
// check if there are the required number of tokens remaining
if program_token_account.amount < ico_token_amount {
msg!("Insufficient tokens remaining in token launch");
return Err(ProgramError::InvalidArgument);
}
...

In order to transfer SOL from one account to another on chain we once again make use of the invoke function, this time executing the transfer instruction, first to send the charity's amount, and then the developer's:

...
// if we have made it this far the transaction we can try transferring the SOL
invoke(
&system_instruction::transfer(joiner_account_info.key, charity_account_info.key, meta.amount_charity),
&[joiner_account_info.clone(), charity_account_info.clone()],
)?;
invoke(
&system_instruction::transfer(joiner_account_info.key, daoplays_account_info.key, meta.amount_dao),
&[joiner_account_info.clone(), daoplays_account_info.clone()],
)?;
...

The next section introduces nothing new. We first transfer the DPTTs to the participant, and then check to see whether they have supporters status in order to create the associated token account for the supporter token (if required) and then send that as well:

...
// and finally transfer the tokens
Self::transfer_tokens(
ico_token_amount,
program_token_account_info,
joiner_token_account_info,
program_data_account_info,
token_program_account_info,
bump_seed
)?;
if supporter && program_supporters_token_account.amount >= 1 {
// check if we need to create the joiners supporter token account
if **joiner_supporters_token_account_info.try_borrow_lamports()? > 0 {
msg!("Users supporter token account is already initialised.");
}
else {
msg!("creating user's supporter token account");
Self::create_token_account(
joiner_account_info,
joiner_account_info,
supporters_token_mint_account_info,
joiner_supporters_token_account_info,
token_program_account_info
)?;
}
Self::transfer_tokens(
1,
program_supporters_token_account_info,
joiner_supporters_token_account_info,
program_data_account_info,
token_program_account_info,
bump_seed
)?;
}
...

The last step in this function is to update all the launch statistics with the new participants payment and donation. We once again use the borsh library to send the current state to the program's data account using the serialize function:

...
// update the data
let charity_index = charity_index_map[meta.charity];
current_state.charity_totals[charity_index] += meta.amount_charity;
current_state.donated_total += meta.amount_charity;
current_state.paid_total += total_paid;
current_state.n_donations += 1;
msg!("Updating current state: {} {} {} {}", current_state.charity_totals[charity_index], current_state.donated_total, current_state.paid_total, current_state.n_donations);
current_state.serialize(&mut &mut program_data_account_info.data.borrow_mut()[..])?;
...

Ending the Token Launch


In order to end the token launch we are simply going to transfer any remaining tokens that are in the programs two token accounts back into our own accounts, and then close those token accounts in order to retrieve the lamports being used to maintain their rent exempt status. We will leave the program and data account up and running so that we can continue to access the donation statistics, though in principle this could also be transferred elsewhere if you so wished. The only new function we will make use of here is the spl_token instruction close_account:

pub fn close_account(
token_program_id: &Pubkey,
account_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
owner_pubkey: &Pubkey,
signer_pubkeys: &[&Pubkey]
) -> Result<Instruction, ProgramError>

This can only be called on a token account that has no tokens remaining, and will take care of transferring the lamports to the destination_pubkey account. The reason that we can't simply do this ourselves with a transfer instruction is that neither we nor the program actually own the token account, the token program does, and so the token program must be the one that sends us the lamports.

We define our close_program_token_account function below, which takes the our program account and it's token account, our wallet and the destination token account and the token program itself:

// in processor.rs
fn close_program_token_account<'a>(
// the program's account info
program_account_info : &AccountInfo<'a>,
// the account info of the token account we want to close
program_token_account_info : &AccountInfo<'a>,
// the destination account for the lamports being retrieved
destination_account_info : &AccountInfo<'a>,
// the destination account for tokens being retrieved
destination_token_account_info : &AccountInfo<'a>,
// the token program
token_program_account_info : &AccountInfo<'a>,
// the bump seed for our program derived address
bump_seed : u8
) -> ProgramResult
...

We first perform a couple of sanity checks. Firstly the destination token account should already exist in our use case as we are simply returning the tokens to their original home, and secondly the program's tokens account should also still be initialised and have it's lamports. If the latter isn't true we must have already closed the account and so we can simply return out of this function as there is nothing left to do.

...
{
// Check the destination token account exists, which it should do if we are the ones that set it up
if **destination_token_account_info.try_borrow_lamports()? > 0 {
msg!("Confirmed destination token account is already initialised.");
}
else {
msg!("destination token account should already exist");
return Err(ProgramError::InvalidAccountData);
}
// And check that we haven't already closed out the program token account
let program_token_account_lamports = **program_token_account_info.try_borrow_lamports()?;
if program_token_account_lamports > 0 {
msg!("Confirmed program token account is still initialised.");
}
else {
msg!("program's token account already closed");
return Ok(());
}
...

As in our join_token_launch function we make use of the unpack_unchecked spl_token function to grab the token account's data so that we can retrieve the remaining token balance. If there are any tokens left we then make use of our transfer_token function to send them to the destination_token_account:

...
let program_token_account = spl_token::state::Account::unpack_unchecked(&program_token_account_info.try_borrow_data()?)?;
msg!("transfer token balance: {}", program_token_account.amount);
if program_token_account.amount > 0 {
Self::transfer_tokens(
program_token_account.amount,
program_token_account_info,
destination_token_account_info,
program_account_info,
token_program_account_info,
bump_seed
)?;
}
...

As the last step we now make use of the spl_token close_account function, and call invoke_signed to allow our program to sign the transaction:

...
msg!("close account and transfer SOL balance: {}", program_token_account_lamports);
let close_token_account_idx = spl_token::instruction::close_account(
token_program_account_info.key,
program_token_account_info.key,
destination_account_info.key,
program_account_info.key,
&[]
)?;
invoke_signed(
&close_token_account_idx,
&[program_token_account_info.clone(), destination_account_info.clone(), program_account_info.clone()],
&[&[b"token_account", &[bump_seed]]]
)?;
Ok(())
}

Our end_token_launch function then simply has to call this function for both the main token account, and the supporters token account after parsing and checking the account details as usual.

A Working Token Launch DApp


Below we have an example interface to our token launch program, the code for which can be found here. It is running on the devnet network so feel free to be generous with your donations as there is no real money involved! We will be using this same approach in the token launch for our first real DApp, so if you have any comments or suggestions, please let us know.

Overview

Total Donated
Loading..
Number Participating
Loading..
Average Paid
Loading..

Account Info




We hope that you have found this post useful, and might be motivated to try and launch your own charitable token launch in the future. If so feel free to follow us on Twitter to keep up to date with future posts!