- Overview
- Introduction
- Set up development environment
- Understand Token Swap instructions
- Practise Token Swap Project
- Features
- Token Swap by library @solana/spl-token-swap
- Next Js for SEO friendly
- Installation
npm install
or
yarn install
- Start Localhost
npm run dev
or
yarn run dev
- View on: http://localhost:3000/
The Token Swap Program allows simple trading of token pairs without a centralized limit order book. The program uses a mathematical formula called “curve” to calculate the price of all trades. Curves aim to mimic normal market dynamics: for example, as traders buy a lot of one token type, the value of the other token type goes up.
Depositors in the pool provide liquidity for the token pair. That liquidity enables trade execution at spot price. In exchange for their liquidity, depositors receive pool tokens, representing their fractional ownership in the pool. During each trade, a program withholds a portion of the input token as a fee. That fee increases the value of pool tokens by being stored in the pool.
This program was heavily inspired by Uniswaphttps://uniswap.org/ and Balancer . More information is available in their excellent documentation and whitepapers.
Before we get into how to create and interact with swap pools on Solana, it’s important we understand the basics of what a swap pool is. A swap pool is an aggregation of two different tokens with the purpose of providing liquidity to facilitate exchange between each token.
Users provide liquidity to these pools by depositing their own tokens into each pool. These users are called liquidity providers. When a liquidity provider (or LP) deposits some tokens to the swap pool, LP-tokens are minted that represent the LP’s fractional ownership in the pool.
Most swap pools charge a trading fee for facilitating each swap. These fees are then paid out to the LP’s in proportion to the amount of liquidity they are providing in the pool. This provides incentive for LP’s to provide liquidity to the pool.
Unlike the Token Program, there is no Solana-maintained deployment of the Token Swap Program. Rather, Solana provides source code for the Token Swap Program as a reference implementation that you can fork and deploy yourself. You can also use a token swap program maintained by a third party organization you trust.
Solana also maintains the @solana/spl-token-swap JS library. This library provides helper functions for interacting with a token swap program. Each helper function takes an argument representing a token swap program ID. As long as the program you use accepts the Token Swap instructions, you can use the @solana/spl-token-swap library with it.
The Token Swap Program is completely customizable for any possible trading curve that implements the CurveCalculator trait. If you would like to implement a new automated market maker, it may be as easy as forking the Token Swap Program and implementing a new curve. The following curves are all provided out of the box for reference.
Trading curves are at the core of how swap pools and AMMs (Automated Market Makers) operate. The trading curve is the function that the Token Swap Program uses to calculate how much of a destination token will be provided given an amount of source token. The curve effectively sets the market price of the tokens in the pool.
The pool we’ll be interacting with in this lesson employs a Constant Product Curve Function. The constant product curve is the well-known Uniswap and Balancer style curve that preserves an invariant on all swaps. This invariant can be expressed as the product of the quantity of token A and token B in the swap pool.
A_total * B_total = invariant
If we have 100 token A and 5,000 token B, our invariant is 500,000.
A_total * B_total = 100 * 5,000 = 500,000 invariant
Now, if a trader wishes to put in a specific amount token A for some amount of token B, the calculation becomes a matter of resolving “B_out” where:
(A_total + A_in) * (B_total - B_out) = invariant
Putting in the 10 token A along with our invariant of half a million, we would need to solve for “B_out” like so:
(100 + 10) * (5,000 - B_out) = 500,000
5,000 - B_out = 500,000 / 110
5,000 - (500,000 / 110) = B_out
B_out = 454,5454....
The product of the amount of token A and token B must always equal a constant, hence the name ‘Constant Product’. More information can be found on the Uniswap whitepaper and the Balancer whitepaper.
The Token-swap JavaScript library comprises:
- A library to interact with the on-chain program
- A test client that exercises the program
- Scripts to facilitate building the program
Using npm:
npm install @solana/spl-token-swap @solana/spl-token
Using yarn:
yarn add @solana/spl-token-swap @solana/spl-token
When setting up the development environment for Token Swap Program, you may find the following tools and resources helpful:
Visual Studio Code (VSCode): VS Code has built-in Git integration, allowing you to manage version control directly from the editor. You can clone repositories, commit changes, switch branches, and resolve merge conflicts.
Git: Git is a commonly used version control system for administering software projects, including Token Swap Program. It permits cloning the Token Swap Program repository, tracking modifications, and collaborating with others.
Token Swap Program Repository: The OpenBook repository on GitHub contains the source code, other resources and examples related to Token Swap.
Blogs and Tutorials: Be on the lookout for blogs and tutorials written by Experienced developers. These resources provide insights, recommendations, and best practices for developing Token Swap applications. Dev.to and Towards Data Science are some websites where such articles can be found.
- To execute a swap
Users can immediately begin trading on a swap pool using the swap instruction. The swap instruction transfers funds from a user’s token account into the swap pool’s token account. The swap pool then mints LP-tokens to the user’s LP-token account.
Since Solana programs require all accounts to be declared in the instruction, users need to gather all account information from the token swap state account: the token A and B accounts, pool token mint, and fee account.
We swap tokens using the TokenSwap.swapInstruction helper function which requires the following arguments:
- Before you can create a swap pool, you’ll need to create a token swap state account. This account will be used to hold information about the swap pool itself.
- To create the token swap state account, you use the SystemProgram instruction createAccount.
const transaction = new Web3.Transaction(); const tokenSwapStateAccount = Web3.Keypair.generate(); const rent = TokenSwap.getMinBalanceRentForExemptTokenSwap(connection); const tokenSwapStateAccountInstruction = await Web3.SystemProgram.createAccount({ newAccountPubkey: tokenSwapStateAccount.publicKey, fromPubkey: wallet.publicKey, lamports: rent, space: TokenSwapLayout.span, programId: TOKEN_SWAP_PROGRAM_ID }); transaction.add(tokenSwapStateAccountInstruction);
- The swap pool authority is the account used to sign for transactions on behalf of the swap program. This account is a Program Derived Address (PDA) derived from the Token Swap Program and the token swap state account.
- PDAs can only be created by their owning program, so you don’t need to create this account directly. You do, however, need to know its public key. You can discover it using the @solana/web3 library’s PublicKey.findProgramAddress function.
const [swapAuthority, bump] = await Web3.PublicKey.findProgramAddress(
[tokenSwapStateAccount.publicKey.toBuffer()],
TOKEN_SWAP_PROGRAM_ID,
)
- It’s a publickey in your wallet. You use the SystemProgram to get public key.
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
const { publicKey, sendTransaction } = useWallet();
- userSource : user token account(Token A) to transfer tokens into the swap
- userDestination : user token account(Token B) to receive tokens sent from the swap pool
Token A and Token B accounts are associated token accounts used for the actual swap pool. These accounts must contain some number of A/B tokens respectively and the swap authority PDA must be marked as the owner of each so that the Token Swap Program can sign for transactions and transfer tokens from each account.
let tokenAAcountAddress = await token.getAssociatedTokenAddress(
tokenAMint, // Mint
swapAuthority, // Onwer
true // Allow owner of curve
)
const tokenAAccountInstruction = await token.createAssociatedTokenAccountInstruction(
wallet.publicKey,
tokenAAccountAddress,
swapAuthority,
tokenAMint
)
- poolSource: swap pool token account (pool token A) to receive tokens transferred from the user
- poolDestination: swap pool token account (pool token B) to send tokens to the user
The pool token account is the account that the initial liquidity pool tokens get minted to when the swap account is first created. Subsequent minting of LP-tokens will be minted directly to the account of the user adding liquidity to the pool. Liquidity pool tokens represent ownership in the deposited liquidity in the pool.
const tokenAccountPool = Web3.Keypair.generate();
const rent = await token.getMinimumBalanceForRentExemptAccount(connection);
const createTokenAccountPoolInstruction = Web3.SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: tokenAccountPool.publicKey,
space: token.ACCOUNT_SIZE,
lamports: rent,
programId: token.TOKEN_PROGRAM_ID
});
const initializeTokenAccountPoolInstruction = token.createInitializeAccountInstruction(
tokenAccountPool.publicKey,
poolTokenMint,
wallet.publicKey
);
transaction.add(createTokenAccountPoolInstruction);
transaction.add(initializeTokenAccountPoolInstruction);
- The pool token mint is the mint of the LP-tokens that represent an LP’s ownership in the pool. You create this mint the way you learned in the Token Program lesson. For the swap pool to work, the mint authority must be the swap authority account.
const poolTokenMint = await token.createMint(
connection,
wallet,
swapAuthority,
null,
2
)
- The pool token fee account is the token account that the fees for the token swaps are paid to.
- Example token account :
HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN
const feeOwner = new web3.PublicKey(`HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN`);
let tokenFeeAccountAddress = await token.getAssociatedTokenAddress(
poolTokenMint, // Mint
feeOwner, // Owner
true, // Allow owner of curve
)
let tokenFeeAccountInstructor = await token.createAssociatedTokenAccountInstruction(
wallet.publicKey, // Payer
tokenFeeAccountAddress, // ATA
feeOwner, // Owner
poolTokenMint, // Mint
)
hostFeeAccount - the token account which receives the host trade fees (optional parameter), set to null if none is provided
import { TokenSwap, TOKEN_SWAP_PROGRAM_ID } from '@solana/spl-token-swap';
import { TokenSwap, TOKEN_SWAP_PROGRAM_ID } from '@solana/spl-token-swap';
- Minimum amount of tokens send to the user token account. This parameter is used to account for slippage. Slippage is the difference between the value of a token when you submit the transaction versus when the order is fulfilled. In this case, the lower the number, the more slippage can possible occur without the transaction failing. Throughout this lesson we’ll use 0 for swaps as calculating slippage is outside the scope of this lesson. In a production app, however, it’s important to let users specify the amount of slippage they’re comfortable with.
- The instruction for swapping token A for token B will look like this:
const instruction = TokenSwap.swapInstruction(
tokenSwapStateAccount,
swapAuthority,
publicKey,
tokenAATA,
pooltokenAAccount,
pooltokenBAccount,
tokenBATA,
poolMint,
feeAccount,
null,
TOKEN_SWAP_PROGRAM_ID,
TOKEN_PROGRAM_ID,
amount * 10 ** tokenAMintInfo.decimals,
0
);
transaction.add(instruction);
For this Project, a token pool of two brand new tokens has been created and is live on Devnet. We’ll walk through building out a frontend UI to interact with this swap pool! Since the pool is already made inside the /utils/constants.ts file, we don’t have to worry about initiating the pool and funding it with tokens. Instead, we’ll focus on building out the instructions for swapping from one token to the other.
Note that our UI has a dropdown to allow users to select which token they would like to swap from, so we will have to create our instruction differently based on what the user selects.
We’ll do this inside the handleTransactionSubmit function of the /components/Swap.tsx file. Once again, we will have to derive the user’s Associated Token Addresses for each token mint (Token A, Token B, and Pool Token) and create the tokenAccountPool if it does not already exist. Additionally, we’ll fetch the data for both the Token A and Token B to account for the decimal precision of the tokens.
if (!publicKey) {
alert("Please connect your wallet!");
return;
}
const tokenAMintInfo = await token.getMint(connection, TokenAMint);
const tokenBMintInfo = await token.getMint(connection, TokenBMint);
const tokenAATA = await token.getAssociatedTokenAddress(
tokenAMint,
publicKey,
);
const tokenBATA = await token.getAssociatedTokenAddress(
tokenBMint,
publicKey,
);
const tokenAccountPool = await token.getAssociatedTokenAddress(
poolMint,
publicKey
);
From here, the user’s input will determine our path of execution. The user’s choice is saved to the mint property, so we’ll use this to branch between each possible instruction.
if (mint1 === "option1" && mint1 !== mint2) {
const instruction = TokenSwap.swapInstruction(
tokenSwapStateAccount, // Token swap state account
swapAuthority, // Token swap state account
publicKey, // public key wallet
tokenAATA, // Token A token account
pooltokenAAccount, // Swap pool token A
pooltokenBAccount, // Swap pool token B
tokenBATA, // Token B token account
poolMint, // Swap pool token mint
feeAccount, // Token fee account
null, // set hostFeeAccount is null
TOKEN_SWAP_PROGRAM_ID,
TOKEN_PROGRAM_ID,
amount * 10 ** tokenAMintInfo.decimals,
0
);
transaction.add(instruction);
}
if (mint1 === "option2" && mint1 !== mint2) {
const instruction = TokenSwap.swapInstruction(
tokenSwapStateAccount,
swapAuthority,
publicKey,
tokenBATA,
pooltokenBAccount,
pooltokenAAccount,
tokenAATA,
poolMint,
feeAccount,
null,
TOKEN_SWAP_PROGRAM_ID,
TOKEN_PROGRAM_ID,
amount * 10 ** tokenBMintInfo.decimals,
0
);
transaction.add(instruction);
}
if (mint1 === mint2) {
alert("Please choose another token!");
} else {
try {
let txid = await sendTransaction(transaction, connection);
alert(
`Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
);
console.log(
`Transaction submitted: https://explorer.solana.com/tx/${txid}?cluster=devnet`
);
} catch (e) {
console.log(JSON.stringify(e));
alert(JSON.stringify(e));
}
}