/** * @flow */ import assert from 'assert'; import BN from 'bn.js'; import * as BufferLayout from 'buffer-layout'; import { Account, PublicKey, SystemProgram, Transaction, TransactionInstruction, } from '@solana/web3.js'; import type {Connection, TransactionSignature} from '@solana/web3.js'; import * as Layout from './layout'; import {sendAndConfirmTransaction} from './util/send-and-confirm-transaction'; /** * Some amount of tokens */ export class Numberu64 extends BN { /** * Convert to Buffer representation */ toBuffer(): 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: 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, ); } } /** * Information about a token swap */ type TokenSwapInfo = {| /** * Token A. The Liquidity token is issued against this value. */ tokenAccountA: PublicKey, /** * Token B */ tokenAccountB: PublicKey, /** * Pool tokens are issued when A or B tokens are deposited * Pool tokens can be withdrawn back to the original A or B token */ tokenPool: PublicKey, /** * Fee numerator */ feesNumerator: Numberu64, /** * Fee denominator */ feesDenominator: Numberu64, /** * Fee ratio applied to the input token amount prior to output calculation */ feeRatio: number, |}; /** * @private */ const TokenSwapLayout = BufferLayout.struct([ BufferLayout.u8('state'), Layout.publicKey('tokenAccountA'), Layout.publicKey('tokenAccountB'), Layout.publicKey('tokenPool'), Layout.uint64('feesDenominator'), Layout.uint64('feesNumerator'), ]); /** * An ERC20-like Token */ export class TokenSwap { /** * @private */ connection: Connection; /** * The public key identifying this token */ tokenSwap: PublicKey; /** * Program Identifier for the Token Swap program */ programId: PublicKey; /** * Fee payer */ payer: Account; /** * Create a Token object attached to the specific token * * @param connection The connection to use * @param token Public key of the token * @param programId Optional token programId, uses the system programId by default * @param payer Payer of fees */ constructor(connection: Connection, tokenSwap: PublicKey, programId: PublicKey, payer: Account) { Object.assign(this, {connection, tokenSwap, programId, 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 { return await connection.getMinimumBalanceForRentExemption( TokenSwapLayout.span, ); } /** * 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 tokenAccountA: The Swap's Token A account * @param tokenAccountB: The Swap's Token B account * @param tokenPool The pool token * @param tokenAccountPool The pool token account * @param tokenProgramId The program id of the token program * @param feeNumerator Numerator of the fee ratio * @param feeDenominator Denominator of the fee ratio * @param programId Program ID of the token-swap program * @return Token object for the newly minted token, Public key of the account holding the total supply of new tokens */ static async createNewTokenSwap( connection: Connection, payer: Account, tokenSwapAccount: Account, authority: PublicKey, tokenAccountA: PublicKey, tokenAccountB: PublicKey, tokenPool: PublicKey, tokenAccountPool: PublicKey, tokenProgramId: PublicKey, feeNumerator: number, feeDenominator: number, programId: PublicKey, ): Promise { let transaction; const tokenSwap = new TokenSwap(connection, tokenSwapAccount.publicKey, programId, payer); // Allocate memory for the account const balanceNeeded = await TokenSwap.getMinBalanceRentForExemptTokenSwap( connection, ); transaction = SystemProgram.createAccount({ fromPubkey: payer.publicKey, newAccountPubkey: tokenSwapAccount.publicKey, lamports: balanceNeeded, space: TokenSwapLayout.span, programId, }); await sendAndConfirmTransaction( 'createAccount', connection, transaction, payer, tokenSwapAccount, ); let keys = [ {pubkey: tokenSwapAccount.publicKey, isSigner: true, isWritable: true}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: tokenAccountA, isSigner: false, isWritable: true}, {pubkey: tokenAccountB, isSigner: false, isWritable: true}, {pubkey: tokenPool, isSigner: false, isWritable: true}, {pubkey: tokenAccountPool, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; const commandDataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), BufferLayout.nu64('feeDenominator'), BufferLayout.nu64('feeNumerator'), ]); let data = Buffer.alloc(1024); { const encodeLength = commandDataLayout.encode( { instruction: 0, // Init instruction feeNumerator, feeDenominator, }, data, ); data = data.slice(0, encodeLength); } transaction = new Transaction().add({ keys, programId, data, }); await sendAndConfirmTransaction( 'Init', connection, transaction, payer, tokenSwapAccount ); return tokenSwap; } /** * Retrieve tokenSwap information */ async getInfo(): Promise { const accountInfo = await this.connection.getAccountInfo(this.tokenSwap); if (accountInfo === null) { throw new Error('Failed to find token swap account'); } if (!accountInfo.owner.equals(this.programId)) { throw new Error( `Invalid token swap owner: ${JSON.stringify(accountInfo.owner)}`, ); } const data = Buffer.from(accountInfo.data); const tokenSwapInfo = TokenSwapLayout.decode(data); if (tokenSwapInfo.state !== 1) { throw new Error(`Invalid token swap state`); } tokenSwapInfo.tokenAccountA = new PublicKey(tokenSwapInfo.tokenAccountA); tokenSwapInfo.tokenAccountB = new PublicKey(tokenSwapInfo.tokenAccountB); tokenSwapInfo.tokenPool = new PublicKey(tokenSwapInfo.tokenPool); tokenSwapInfo.feesNumerator = Numberu64.fromBuffer(tokenSwapInfo.feesNumerator); tokenSwapInfo.feesDenominator = Numberu64.fromBuffer(tokenSwapInfo.feesDenominator); tokenSwapInfo.feeRatio = tokenSwapInfo.feesNumerator.toNumber() / tokenSwapInfo.feesDenominator.toNumber(); return tokenSwapInfo; } /** * Swap the tokens in the pool * * @param authority Authority * @param delegate Delegate account to transfer from * @param source Source account associated with delegate * @param into Base account to swap into, must be a source token * @param from Base account to swap from, must be a destination token * @param dest Destination token * @param tokenProgramId Token program id * @param amount Amount to transfer from source account */ async swap( authority: PublicKey, delegate: PublicKey, source: PublicKey, into: PublicKey, from: PublicKey, destination: PublicKey, tokenProgramId: PublicKey, amount: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'swap', this.connection, new Transaction().add( this.swapInstruction( authority, delegate, source, into, from, destination, tokenProgramId, amount, ), ), this.payer, ); } swapInstruction( authority: PublicKey, delegate: PublicKey, source: PublicKey, into: PublicKey, from: PublicKey, destination: PublicKey, tokenProgramId: PublicKey, amount: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), Layout.uint64('amount'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 1, // Swap instruction amount: new Numberu64(amount).toBuffer(), }, data, ); const keys = [ {pubkey: this.tokenSwap, isSigner: false, isWritable: false}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: delegate, isSigner: false, isWritable: true}, {pubkey: source, isSigner: false, isWritable: true}, {pubkey: into, isSigner: false, isWritable: true}, {pubkey: from, isSigner: false, isWritable: true}, {pubkey: destination, isSigner: false, isWritable: true}, {pubkey: tokenProgramId, isSigner: false, isWritable: false}, ]; return new TransactionInstruction({ keys, programId: this.programId, data, }); } /** * Deposit some tokens into the pool * * @param authority Authority * @param delegateA Delegate account to transfer token A from * @param sourceA Source account associated with delegate account A * @param delegateB Delegate account to transfer token B from * @param sourceB Source account associated with delegate account A * @param intoA Base account A to deposit into * @param intoB Base account B to deposit into * @param poolToken Pool token * @param poolAccount Pool account to deposit the generated tokens * @param tokenProgramId Token program id * @param amount Amount of token A to transfer, token B amount is set by the exchange rate */ async deposit( authority: PublicKey, delegateA: PublicKey, sourceA: PublicKey, delegateB: PublicKey, sourceB: PublicKey, intoA: PublicKey, intoB: PublicKey, poolToken: PublicKey, poolAccount: PublicKey, tokenProgramId: PublicKey, amount: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'deposit', this.connection, new Transaction().add( this.depositInstruction( authority, delegateA, sourceA, delegateB, sourceB, intoA, intoB, poolToken, poolAccount, tokenProgramId, amount, ), ), this.payer, ); } depositInstruction( authority: PublicKey, delegateA: PublicKey, sourceA: PublicKey, delegateB: PublicKey, sourceB: PublicKey, intoA: PublicKey, intoB: PublicKey, poolToken: PublicKey, poolAccount: PublicKey, tokenProgramId: PublicKey, amount: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), Layout.uint64('amount'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 2, // Deposit instruction amount: new Numberu64(amount).toBuffer(), }, data, ); const keys = [ {pubkey: this.tokenSwap, isSigner: false, isWritable: false}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: delegateA, isSigner: false, isWritable: true}, {pubkey: sourceA, isSigner: false, isWritable: true}, {pubkey: delegateB, 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: this.programId, data, }); } /** * Withdraw the token from the pool at the current ratio * * @param authority Authority * @param delegatePoolAccount Delegate pool account * @param sourcePoolAccount Source account associated with delegate * @param poolToken Pool token * @param fromA Base account A to withdraw from * @param fromB Base account B to withdraw from * @param userAccountA Token A user account * @param userAccountB token B user account * @param tokenProgramId Token program id * @param amount Amount of token A to transfer, token B amount is set by the exchange rate */ async withdraw( authority: PublicKey, delegatePoolAccount: PublicKey, sourcePoolAccount: PublicKey, poolToken: PublicKey, fromA: PublicKey, fromB: PublicKey, userAccountA: PublicKey, userAccountB: PublicKey, tokenProgramId: PublicKey, amount: number | Numberu64, ): Promise { return await sendAndConfirmTransaction( 'withdraw', this.connection, new Transaction().add( this.withdrawInstruction( authority, delegatePoolAccount, sourcePoolAccount, poolToken, fromA, fromB, userAccountA, userAccountB, tokenProgramId, amount, ), ), this.payer, ); } withdrawInstruction( authority: PublicKey, delegatePoolAccount: PublicKey, sourcePoolAccount: PublicKey, poolToken: PublicKey, fromA: PublicKey, fromB: PublicKey, userAccountA: PublicKey, userAccountB: PublicKey, tokenProgramId: PublicKey, amount: number | Numberu64, ): TransactionInstruction { const dataLayout = BufferLayout.struct([ BufferLayout.u8('instruction'), Layout.uint64('amount'), ]); const data = Buffer.alloc(dataLayout.span); dataLayout.encode( { instruction: 3, // Withdraw instruction amount: new Numberu64(amount).toBuffer(), }, data, ); const keys = [ {pubkey: this.tokenSwap, isSigner: false, isWritable: false}, {pubkey: authority, isSigner: false, isWritable: false}, {pubkey: delegatePoolAccount, isSigner: false, isWritable: true}, {pubkey: sourcePoolAccount, isSigner: false, isWritable: true}, {pubkey: poolToken, 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: tokenProgramId, isSigner: false, isWritable: false}, ]; return new TransactionInstruction({ keys, programId: this.programId, data, }); } }