solana-program-library/token-swap/js/client/token-swap.js

777 lines
22 KiB
JavaScript

/**
* @flow
*/
import assert from 'assert';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
import type {Connection, TransactionSignature} from '@solana/web3.js';
import {
Account,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import * as Layout from './layout';
import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction';
import {loadAccount} from './util/account';
/**
* Some amount of tokens
*/
export class Numberu64 extends BN {
/**
* Convert to Buffer representation
*/
toBuffer(): typeof Buffer {
const a = super.toArray().reverse();
const b = Buffer.from(a);
if (b.length === 8) {
return b;
}
assert(b.length < 8, 'Numberu64 too large');
const zeroPad = Buffer.alloc(8);
b.copy(zeroPad);
return zeroPad;
}
/**
* Construct a Numberu64 from Buffer representation
*/
static fromBuffer(buffer: typeof Buffer): Numberu64 {
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
return new BN(
[...buffer]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
}
}
/**
* @private
*/
export const TokenSwapLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('isInitialized'),
BufferLayout.u8('nonce'),
Layout.publicKey('tokenProgramId'),
Layout.publicKey('tokenAccountA'),
Layout.publicKey('tokenAccountB'),
Layout.publicKey('tokenPool'),
Layout.publicKey('mintA'),
Layout.publicKey('mintB'),
Layout.publicKey('feeAccount'),
BufferLayout.u8('curveType'),
Layout.uint64('tradeFeeNumerator'),
Layout.uint64('tradeFeeDenominator'),
Layout.uint64('ownerTradeFeeNumerator'),
Layout.uint64('ownerTradeFeeDenominator'),
Layout.uint64('ownerWithdrawFeeNumerator'),
Layout.uint64('ownerWithdrawFeeDenominator'),
BufferLayout.blob(16, 'padding'),
],
);
export const CurveType = Object.freeze({
ConstantProduct: 0, // Constant product curve, Uniswap-style
Flat: 1, // Flat curve, always 1:1 trades
});
/**
* A program to exchange tokens against a pool of liquidity
*/
export class TokenSwap {
/**
* @private
*/
connection: Connection;
/**
* Program Identifier for the Swap program
*/
swapProgramId: PublicKey;
/**
* Program Identifier for the Token program
*/
tokenProgramId: PublicKey;
/**
* The public key identifying this swap program
*/
tokenSwap: PublicKey;
/**
* The public key for the liquidity pool token mint
*/
poolToken: PublicKey;
/**
* The public key for the fee account receiving trade and/or withdrawal fees
*/
feeAccount: PublicKey;
/**
* Authority
*/
authority: PublicKey;
/**
* The public key for the first token account of the trading pair
*/
tokenAccountA: PublicKey;
/**
* The public key for the second token account of the trading pair
*/
tokenAccountB: PublicKey;
/**
* The public key for the mint of the first token account of the trading pair
*/
mintA: PublicKey;
/**
* The public key for the mint of the second token account of the trading pair
*/
mintB: PublicKey;
/**
* Trading fee numerator
*/
tradeFeeNumerator: Numberu64;
/**
* Trading fee denominator
*/
tradeFeeDenominator: Numberu64;
/**
* Owner trading fee numerator
*/
ownerTradeFeeNumerator: Numberu64;
/**
* Owner trading fee denominator
*/
ownerTradeFeeDenominator: Numberu64;
/**
* Owner withdraw fee numerator
*/
ownerWithdrawFeeNumerator: Numberu64;
/**
* Owner withdraw fee denominator
*/
ownerWithdrawFeeDenominator: Numberu64;
/**
* CurveType, current options are:
*/
curveType: number;
/**
* Fee payer
*/
payer: Account;
/**
* Create a Token object attached to the specific token
*
* @param connection The connection to use
* @param tokenSwap The token swap account
* @param swapProgramId The program ID of the token-swap program
* @param tokenProgramId The program ID of the token program
* @param poolToken The pool token
* @param authority The authority over the swap and accounts
* @param tokenAccountA: The token swap's Token A account
* @param tokenAccountB: The token swap's Token B account
* @param payer Pays for the transaction
*/
constructor(
connection: Connection,
tokenSwap: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
poolToken: PublicKey,
feeAccount: PublicKey,
authority: PublicKey,
tokenAccountA: PublicKey,
tokenAccountB: PublicKey,
mintA: PublicKey,
mintB: PublicKey,
curveType: number,
tradeFeeNumerator: Numberu64,
tradeFeeDenominator: Numberu64,
ownerTradeFeeNumerator: Numberu64,
ownerTradeFeeDenominator: Numberu64,
ownerWithdrawFeeNumerator: Numberu64,
ownerWithdrawFeeDenominator: Numberu64,
payer: Account,
) {
Object.assign(this, {
connection,
tokenSwap,
swapProgramId,
tokenProgramId,
poolToken,
feeAccount,
authority,
tokenAccountA,
tokenAccountB,
mintA,
mintB,
curveType,
tradeFeeNumerator,
tradeFeeDenominator,
ownerTradeFeeNumerator,
ownerTradeFeeDenominator,
ownerWithdrawFeeNumerator,
ownerWithdrawFeeDenominator,
payer,
});
}
/**
* Get the minimum balance for the token swap account to be rent exempt
*
* @return Number of lamports required
*/
static async getMinBalanceRentForExemptTokenSwap(
connection: Connection,
): Promise<number> {
return await connection.getMinimumBalanceForRentExemption(
TokenSwapLayout.span,
);
}
static createInitSwapInstruction(
tokenSwapAccount: Account,
authority: PublicKey,
tokenAccountA: PublicKey,
tokenAccountB: PublicKey,
tokenPool: PublicKey,
feeAccount: PublicKey,
tokenAccountPool: PublicKey,
tokenProgramId: PublicKey,
swapProgramId: PublicKey,
nonce: number,
curveType: number,
tradeFeeNumerator: number,
tradeFeeDenominator: number,
ownerTradeFeeNumerator: number,
ownerTradeFeeDenominator: number,
ownerWithdrawFeeNumerator: number,
ownerWithdrawFeeDenominator: number,
): TransactionInstruction {
const keys = [
{pubkey: tokenSwapAccount.publicKey, isSigner: false, isWritable: true},
{pubkey: authority, isSigner: false, isWritable: false},
{pubkey: tokenAccountA, isSigner: false, isWritable: false},
{pubkey: tokenAccountB, isSigner: false, isWritable: false},
{pubkey: tokenPool, isSigner: false, isWritable: true},
{pubkey: feeAccount, isSigner: false, isWritable: false},
{pubkey: tokenAccountPool, isSigner: false, isWritable: true},
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
];
const commandDataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.u8('nonce'),
BufferLayout.u8('curveType'),
BufferLayout.nu64('tradeFeeNumerator'),
BufferLayout.nu64('tradeFeeDenominator'),
BufferLayout.nu64('ownerTradeFeeNumerator'),
BufferLayout.nu64('ownerTradeFeeDenominator'),
BufferLayout.nu64('ownerWithdrawFeeNumerator'),
BufferLayout.nu64('ownerWithdrawFeeDenominator'),
BufferLayout.blob(16, 'padding'),
]);
let data = Buffer.alloc(1024);
{
const encodeLength = commandDataLayout.encode(
{
instruction: 0, // InitializeSwap instruction
nonce,
curveType,
tradeFeeNumerator,
tradeFeeDenominator,
ownerTradeFeeNumerator,
ownerTradeFeeDenominator,
ownerWithdrawFeeNumerator,
ownerWithdrawFeeDenominator,
},
data,
);
data = data.slice(0, encodeLength);
}
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
}
static async loadTokenSwap(
connection: Connection,
address: PublicKey,
programId: PublicKey,
payer: Account,
): Promise<TokenSwap> {
const data = await loadAccount(connection, address, programId);
const tokenSwapData = TokenSwapLayout.decode(data);
if (!tokenSwapData.isInitialized) {
throw new Error(`Invalid token swap state`);
}
const [authority] = await PublicKey.findProgramAddress(
[address.toBuffer()],
programId,
);
const poolToken = new PublicKey(tokenSwapData.tokenPool);
const feeAccount = new PublicKey(tokenSwapData.feeAccount);
const tokenAccountA = new PublicKey(tokenSwapData.tokenAccountA);
const tokenAccountB = new PublicKey(tokenSwapData.tokenAccountB);
const mintA = new PublicKey(tokenSwapData.mintA);
const mintB = new PublicKey(tokenSwapData.mintB);
const tokenProgramId = new PublicKey(tokenSwapData.tokenProgramId);
const tradeFeeNumerator = Numberu64.fromBuffer(
tokenSwapData.tradeFeeNumerator,
);
const tradeFeeDenominator = Numberu64.fromBuffer(
tokenSwapData.tradeFeeDenominator,
);
const ownerTradeFeeNumerator = Numberu64.fromBuffer(
tokenSwapData.ownerTradeFeeNumerator,
);
const ownerTradeFeeDenominator = Numberu64.fromBuffer(
tokenSwapData.ownerTradeFeeDenominator,
);
const ownerWithdrawFeeNumerator = Numberu64.fromBuffer(
tokenSwapData.ownerWithdrawFeeNumerator,
);
const ownerWithdrawFeeDenominator = Numberu64.fromBuffer(
tokenSwapData.ownerWithdrawFeeDenominator,
);
const curveType = tokenSwapData.curveType;
return new TokenSwap(
connection,
address,
programId,
tokenProgramId,
poolToken,
feeAccount,
authority,
tokenAccountA,
tokenAccountB,
mintA,
mintB,
curveType,
tradeFeeNumerator,
tradeFeeDenominator,
ownerTradeFeeNumerator,
ownerTradeFeeDenominator,
ownerWithdrawFeeNumerator,
ownerWithdrawFeeDenominator,
payer,
);
}
/**
* Create a new Token Swap
*
* @param connection The connection to use
* @param payer Pays for the transaction
* @param tokenSwapAccount The token swap account
* @param authority The authority over the swap and accounts
* @param nonce The nonce used to generate the authority
* @param tokenAccountA: The token swap's Token A account
* @param tokenAccountB: The token swap's Token B account
* @param poolToken The pool token
* @param tokenAccountPool The token swap's pool token account
* @param tokenProgramId The program ID of the token program
* @param swapProgramId The program ID of the token-swap program
* @param feeNumerator Numerator of the fee ratio
* @param feeDenominator Denominator of the fee ratio
* @return Token object for the newly minted token, Public key of the account holding the total supply of new tokens
*/
static async createTokenSwap(
connection: Connection,
payer: Account,
tokenSwapAccount: Account,
authority: PublicKey,
tokenAccountA: PublicKey,
tokenAccountB: PublicKey,
poolToken: PublicKey,
mintA: PublicKey,
mintB: PublicKey,
feeAccount: PublicKey,
tokenAccountPool: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
nonce: number,
curveType: number,
tradeFeeNumerator: number,
tradeFeeDenominator: number,
ownerTradeFeeNumerator: number,
ownerTradeFeeDenominator: number,
ownerWithdrawFeeNumerator: number,
ownerWithdrawFeeDenominator: number,
): Promise<TokenSwap> {
let transaction;
const tokenSwap = new TokenSwap(
connection,
tokenSwapAccount.publicKey,
swapProgramId,
tokenProgramId,
poolToken,
feeAccount,
authority,
tokenAccountA,
tokenAccountB,
mintA,
mintB,
curveType,
new Numberu64(tradeFeeNumerator),
new Numberu64(tradeFeeDenominator),
new Numberu64(ownerTradeFeeNumerator),
new Numberu64(ownerTradeFeeDenominator),
new Numberu64(ownerWithdrawFeeNumerator),
new Numberu64(ownerWithdrawFeeDenominator),
payer,
);
// Allocate memory for the account
const balanceNeeded = await TokenSwap.getMinBalanceRentForExemptTokenSwap(
connection,
);
transaction = new Transaction();
transaction.add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: tokenSwapAccount.publicKey,
lamports: balanceNeeded,
space: TokenSwapLayout.span,
programId: swapProgramId,
}),
);
const instruction = TokenSwap.createInitSwapInstruction(
tokenSwapAccount,
authority,
tokenAccountA,
tokenAccountB,
poolToken,
feeAccount,
tokenAccountPool,
tokenProgramId,
swapProgramId,
nonce,
curveType,
tradeFeeNumerator,
tradeFeeDenominator,
ownerTradeFeeNumerator,
ownerTradeFeeDenominator,
ownerWithdrawFeeNumerator,
ownerWithdrawFeeDenominator,
);
transaction.add(instruction);
await sendAndConfirmTransaction(
'createAccount and InitializeSwap',
connection,
transaction,
payer,
tokenSwapAccount,
);
return tokenSwap;
}
/**
* Swap token A for token B
*
* @param userSource User's source token account
* @param poolSource Pool's source token account
* @param poolDestination Pool's destination token account
* @param userDestination User's destination token account
* @param amountIn Amount to transfer from source account
* @param minimumAmountOut Minimum amount of tokens the user will receive
*/
async swap(
userSource: PublicKey,
poolSource: PublicKey,
poolDestination: PublicKey,
userDestination: PublicKey,
amountIn: number | Numberu64,
minimumAmountOut: number | Numberu64,
): Promise<TransactionSignature> {
return await sendAndConfirmTransaction(
'swap',
this.connection,
new Transaction().add(
TokenSwap.swapInstruction(
this.tokenSwap,
this.authority,
userSource,
poolSource,
poolDestination,
userDestination,
this.poolToken,
this.feeAccount,
this.swapProgramId,
this.tokenProgramId,
amountIn,
minimumAmountOut,
),
),
this.payer,
);
}
static swapInstruction(
tokenSwap: PublicKey,
authority: PublicKey,
userSource: PublicKey,
poolSource: PublicKey,
poolDestination: PublicKey,
userDestination: PublicKey,
poolMint: PublicKey,
feeAccount: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
amountIn: number | Numberu64,
minimumAmountOut: number | Numberu64,
): TransactionInstruction {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('amountIn'),
Layout.uint64('minimumAmountOut'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 1, // Swap instruction
amountIn: new Numberu64(amountIn).toBuffer(),
minimumAmountOut: new Numberu64(minimumAmountOut).toBuffer(),
},
data,
);
const keys = [
{pubkey: tokenSwap, isSigner: false, isWritable: false},
{pubkey: authority, isSigner: false, isWritable: false},
{pubkey: userSource, isSigner: false, isWritable: true},
{pubkey: poolSource, isSigner: false, isWritable: true},
{pubkey: poolDestination, isSigner: false, isWritable: true},
{pubkey: userDestination, isSigner: false, isWritable: true},
{pubkey: poolMint, isSigner: false, isWritable: true},
{pubkey: feeAccount, isSigner: false, isWritable: true},
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
];
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
}
/**
* Deposit tokens into the pool
* @param userAccountA User account for token A
* @param userAccountB User account for token B
* @param poolAccount User account for pool token
* @param poolTokenAmount Amount of pool tokens to mint
* @param maximumTokenA The maximum amount of token A to deposit
* @param maximumTokenB The maximum amount of token B to deposit
*/
async deposit(
userAccountA: PublicKey,
userAccountB: PublicKey,
poolAccount: PublicKey,
poolTokenAmount: number | Numberu64,
maximumTokenA: number | Numberu64,
maximumTokenB: number | Numberu64,
): Promise<TransactionSignature> {
return await sendAndConfirmTransaction(
'deposit',
this.connection,
new Transaction().add(
TokenSwap.depositInstruction(
this.tokenSwap,
this.authority,
userAccountA,
userAccountB,
this.tokenAccountA,
this.tokenAccountB,
this.poolToken,
poolAccount,
this.swapProgramId,
this.tokenProgramId,
poolTokenAmount,
maximumTokenA,
maximumTokenB,
),
),
this.payer,
);
}
static depositInstruction(
tokenSwap: PublicKey,
authority: PublicKey,
sourceA: PublicKey,
sourceB: PublicKey,
intoA: PublicKey,
intoB: PublicKey,
poolToken: PublicKey,
poolAccount: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
poolTokenAmount: number | Numberu64,
maximumTokenA: number | Numberu64,
maximumTokenB: number | Numberu64,
): TransactionInstruction {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('poolTokenAmount'),
Layout.uint64('maximumTokenA'),
Layout.uint64('maximumTokenB'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 2, // Deposit instruction
poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(),
maximumTokenA: new Numberu64(maximumTokenA).toBuffer(),
maximumTokenB: new Numberu64(maximumTokenB).toBuffer(),
},
data,
);
const keys = [
{pubkey: tokenSwap, isSigner: false, isWritable: false},
{pubkey: authority, isSigner: false, isWritable: false},
{pubkey: sourceA, isSigner: false, isWritable: true},
{pubkey: sourceB, isSigner: false, isWritable: true},
{pubkey: intoA, isSigner: false, isWritable: true},
{pubkey: intoB, isSigner: false, isWritable: true},
{pubkey: poolToken, isSigner: false, isWritable: true},
{pubkey: poolAccount, isSigner: false, isWritable: true},
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
];
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
}
/**
* Withdraw tokens from the pool
*
* @param userAccountA User account for token A
* @param userAccountB User account for token B
* @param poolAccount User account for pool token
* @param poolTokenAmount Amount of pool tokens to burn
* @param minimumTokenA The minimum amount of token A to withdraw
* @param minimumTokenB The minimum amount of token B to withdraw
*/
async withdraw(
userAccountA: PublicKey,
userAccountB: PublicKey,
poolAccount: PublicKey,
poolTokenAmount: number | Numberu64,
minimumTokenA: number | Numberu64,
minimumTokenB: number | Numberu64,
): Promise<TransactionSignature> {
return await sendAndConfirmTransaction(
'withdraw',
this.connection,
new Transaction().add(
TokenSwap.withdrawInstruction(
this.tokenSwap,
this.authority,
this.poolToken,
this.feeAccount,
poolAccount,
this.tokenAccountA,
this.tokenAccountB,
userAccountA,
userAccountB,
this.swapProgramId,
this.tokenProgramId,
poolTokenAmount,
minimumTokenA,
minimumTokenB,
),
),
this.payer,
);
}
static withdrawInstruction(
tokenSwap: PublicKey,
authority: PublicKey,
poolMint: PublicKey,
feeAccount: PublicKey,
sourcePoolAccount: PublicKey,
fromA: PublicKey,
fromB: PublicKey,
userAccountA: PublicKey,
userAccountB: PublicKey,
swapProgramId: PublicKey,
tokenProgramId: PublicKey,
poolTokenAmount: number | Numberu64,
minimumTokenA: number | Numberu64,
minimumTokenB: number | Numberu64,
): TransactionInstruction {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
Layout.uint64('poolTokenAmount'),
Layout.uint64('minimumTokenA'),
Layout.uint64('minimumTokenB'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 3, // Withdraw instruction
poolTokenAmount: new Numberu64(poolTokenAmount).toBuffer(),
minimumTokenA: new Numberu64(minimumTokenA).toBuffer(),
minimumTokenB: new Numberu64(minimumTokenB).toBuffer(),
},
data,
);
const keys = [
{pubkey: tokenSwap, isSigner: false, isWritable: false},
{pubkey: authority, isSigner: false, isWritable: false},
{pubkey: poolMint, isSigner: false, isWritable: true},
{pubkey: sourcePoolAccount, isSigner: false, isWritable: true},
{pubkey: fromA, isSigner: false, isWritable: true},
{pubkey: fromB, isSigner: false, isWritable: true},
{pubkey: userAccountA, isSigner: false, isWritable: true},
{pubkey: userAccountB, isSigner: false, isWritable: true},
{pubkey: feeAccount, isSigner: false, isWritable: true},
{pubkey: tokenProgramId, isSigner: false, isWritable: false},
];
return new TransactionInstruction({
keys,
programId: swapProgramId,
data,
});
}
}