@unloc-xyz/unloc-sdk
Version:
522 lines (484 loc) • 18.2 kB
text/typescript
import {
Account,
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionSignature
} from '@solana/web3.js'
import {ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID} from "@solana/spl-token";
import * as anchor from "@project-serum/anchor";
import {DexInstructions, Market as MarketSerum, TokenInstructions,} from '@project-serum/serum';
import {Liquidity, Market as raydiumSerum, Spl, SPL_MINT_LAYOUT} from "@raydium-io/raydium-sdk";
import {LiquidityAssociatedPoolKeys} from "@raydium-io/raydium-sdk/src/liquidity"
export async function getAssociatedPoolKeys({
programId,
serumProgramId,
marketId,
baseMint,
quoteMint,
}: {
programId: PublicKey;
serumProgramId: PublicKey;
marketId: PublicKey;
baseMint: PublicKey;
quoteMint: PublicKey;
}): Promise<LiquidityAssociatedPoolKeys> {
const id = await Liquidity.getAssociatedId({programId, marketId});
const lpMint = await Liquidity.getAssociatedLpMint({programId, marketId});
const {publicKey: authority, nonce} = await Liquidity.getAssociatedAuthority({programId});
const baseVault = await Liquidity.getAssociatedBaseVault({programId, marketId});
const quoteVault = await Liquidity.getAssociatedQuoteVault({programId, marketId});
const lpVault = await Liquidity.getAssociatedLpVault({programId, marketId});
const openOrders = await Liquidity.getAssociatedOpenOrders({programId, marketId});
const targetOrders = await Liquidity.getAssociatedTargetOrders({programId, marketId});
const withdrawQueue = await Liquidity.getAssociatedWithdrawQueue({programId, marketId});
const {publicKey: marketAuthority} = await raydiumSerum.getAssociatedAuthority({
programId: serumProgramId,
marketId,
});
return {
// base
id,
baseMint,
quoteMint,
lpMint,
// version
version: 4,
programId,
// keys
authority,
nonce,
baseVault,
quoteVault,
lpVault,
openOrders,
targetOrders,
withdrawQueue,
// market version
marketVersion: 4,
marketProgramId: serumProgramId,
// market keys
marketId,
marketAuthority,
};
}
export async function createAssociatedTokenAccountIfNotExist(
owner: PublicKey,
mint: PublicKey,
transaction: Transaction,
conn: any,
) {
const associatedAccount = await Spl.getAssociatedTokenAccount({mint, owner})
const payer = owner
const associatedAccountInfo = await conn.getAccountInfo(associatedAccount)
if (!associatedAccountInfo) {
transaction.add(Spl.makeCreateAssociatedTokenAccountInstruction({
mint,
associatedAccount,
owner,
payer
})
);
}
return associatedAccount
}
export async function sendTransaction(
connection: Connection,
wallet: any,
transaction: Transaction,
signers: Array<Account> = []
) {
const txid: TransactionSignature = await wallet.sendTransaction(transaction, connection, {
signers,
skipPreflight: true,
preflightCommitment: "confirmed"
})
return txid
}
export function getBigNumber(num: any) {
return num === undefined || num === null ? 0 : parseFloat(num.toString())
}
export async function getMarket(conn: any, marketAddress: string, serumProgramId: string): Promise<Market> {
try {
const marketAddressPubKey = new PublicKey(marketAddress)
const market = await Market.load(conn, marketAddressPubKey, undefined, new PublicKey(serumProgramId))
return market
} catch (error: any) {
console.log("get market err: ", error)
throw error;
}
}
export class Market extends MarketSerum {
public baseVault: PublicKey | null = null
public quoteVault: PublicKey | null = null
public requestQueue: PublicKey | null = null
public eventQueue: PublicKey | null = null
public bids: PublicKey | null = null
public asks: PublicKey | null = null
public baseLotSize: number = 0
public quoteLotSize: number = 0
// private _decoded: any
public quoteMint: PublicKey | null = null
public baseMint: PublicKey | null = null
public vaultSignerNonce: Number | null = null
static async load(connection: Connection, address: PublicKey, options: any = {}, programId: PublicKey) {
const {owner, data} = throwIfNull(await connection.getAccountInfo(address), 'Market not found')
if (!owner.equals(programId)) {
throw new Error('Address not owned by program: ' + owner.toBase58())
}
const decoded = this.getLayout(programId).decode(data)
if (!decoded.accountFlags.initialized || !decoded.accountFlags.market || !decoded.ownAddress.equals(address)) {
throw new Error('Invalid market')
}
const [baseMintDecimals, quoteMintDecimals] = await Promise.all([
getMintDecimals(connection, decoded.baseMint),
getMintDecimals(connection, decoded.quoteMint)
])
const market = new Market(decoded, baseMintDecimals, quoteMintDecimals, options, programId)
// market._decoded = decoded
market.baseLotSize = decoded.baseLotSize
market.quoteLotSize = decoded.quoteLotSize
market.baseVault = decoded.baseVault
market.quoteVault = decoded.quoteVault
market.requestQueue = decoded.requestQueue
market.eventQueue = decoded.eventQueue
market.bids = decoded.bids
market.asks = decoded.asks
market.quoteMint = decoded.quoteMint
market.baseMint = decoded.baseMint
market.vaultSignerNonce = decoded.vaultSignerNonce
return market
}
}
export async function getMintDecimals(connection: Connection, mint: PublicKey): Promise<number> {
const {data} = throwIfNull(await connection.getAccountInfo(mint), 'mint not found')
const {decimals} = SPL_MINT_LAYOUT.decode(data)
return decimals
}
function throwIfNull<T>(value: T | null, message = 'account not found'): T {
if (value === null) {
throw new Error(message)
}
return value
}
export async function getFilteredTokenAccountsByOwner(
connection: Connection,
programId: PublicKey,
mint: PublicKey
): Promise<{ context: {}; value: [] }> {
// @ts-ignore
const resp = await connection._rpcRequest('getTokenAccountsByOwner', [
programId.toBase58(),
{
mint: mint.toBase58()
},
{
encoding: 'jsonParsed'
}
])
if (resp.error) {
throw new Error(resp.error.message)
}
return resp.result
}
export async function checkTxid(conn: Connection, txid: any) {
let txidSuccessFlag = 0
await conn.onSignature(txid, function (_signatureResult: any, _context: any) {
if (_signatureResult.err) {
txidSuccessFlag = -1
} else {
txidSuccessFlag = 1
}
})
const timeAwait = new Date().getTime()
let outOfWhile = false
while (!outOfWhile) {
console.log('wait txid:', txid, outOfWhile, txidSuccessFlag, (new Date().getTime() - timeAwait) / 1000)
if (txidSuccessFlag !== 0) {
outOfWhile = true
}
await new Promise((resolve) => setTimeout(resolve, 1000))
}
if (txidSuccessFlag !== 1) {
throw new Error('Transaction failed')
}
}
export async function createMintPair(provider: anchor.AnchorProvider) {
const connection = provider.connection
const wallet = (provider as any).wallet
const tokenA = await Token.createMint(
connection,
wallet.payer,
wallet.publicKey,
null,
9,
TOKEN_PROGRAM_ID
);
const tokenB = await Token.createMint(
connection,
wallet.payer,
wallet.publicKey,
null,
9,
TOKEN_PROGRAM_ID
);
const tokenAMintAddress = tokenA.publicKey
const tokenBMintAddress = tokenB.publicKey
const associatedTokenA = await Token.getAssociatedTokenAddress(ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, tokenAMintAddress, wallet.publicKey, true);
const associatedTokenB = await Token.getAssociatedTokenAddress(ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, tokenBMintAddress, wallet.publicKey, true);
const tx1 = new Transaction();
tx1.add(
Token.createAssociatedTokenAccountInstruction(ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, tokenAMintAddress, associatedTokenA, wallet.publicKey, wallet.publicKey),
Token.createAssociatedTokenAccountInstruction(ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, tokenBMintAddress, associatedTokenB, wallet.publicKey, wallet.publicKey),
);
let txid = await provider.sendAndConfirm(tx1)
sleep(5000)
const tx2 = new Transaction();
tx2.add(
Token.createMintToInstruction(TOKEN_PROGRAM_ID, tokenAMintAddress, associatedTokenA, wallet.publicKey, [wallet.payer], 100000000000000),
Token.createMintToInstruction(TOKEN_PROGRAM_ID, tokenBMintAddress, associatedTokenB, wallet.publicKey, [wallet.payer], 200000000000000)
)
txid = await provider.sendAndConfirm(tx2)
console.log("create tokenA: ", tokenA.publicKey.toString(), " tokenB: ", tokenBMintAddress.toString(), "mint txid: ", txid)
sleep(5000)
return {tokenAMintAddress, tokenBMintAddress}
}
export async function createSerumMarket({
connection,
wallet,
baseMint,
quoteMint,
baseLotSize,
quoteLotSize,
dexProgram,
market
}: {
connection: Connection;
wallet: anchor.Wallet;
baseMint: PublicKey;
quoteMint: PublicKey;
baseLotSize: number;
quoteLotSize: number;
dexProgram: PublicKey;
market: Keypair;
}): Promise<SerumMarketInfo> {
const requestQueue = new Keypair();
const eventQueue = new Keypair();
const bids = new Keypair();
const asks = new Keypair();
const baseVault = new Keypair();
const quoteVault = new Keypair();
const feeRateBps = 0;
const quoteDustThreshold = new anchor.BN(10);
const {vaultOwner, vaultNonce} = await getVaultOwnerAndNonce(market.publicKey, dexProgram);
const tx1 = new Transaction();
tx1.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: baseVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TokenInstructions.TOKEN_PROGRAM_ID,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: quoteVault.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(165),
space: 165,
programId: TokenInstructions.TOKEN_PROGRAM_ID,
}),
TokenInstructions.initializeAccount({
account: baseVault.publicKey,
mint: baseMint,
owner: vaultOwner,
}),
TokenInstructions.initializeAccount({
account: quoteVault.publicKey,
mint: quoteMint,
owner: vaultOwner,
}),
);
const tx2 = new Transaction();
tx2.add(
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: market.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(
Market.getLayout(dexProgram).span,
),
space: Market.getLayout(dexProgram).span,
programId: dexProgram,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: requestQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(5120 + 12),
space: 5120 + 12,
programId: dexProgram,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: eventQueue.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(262144 + 12),
space: 262144 + 12,
programId: dexProgram,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: bids.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgram,
}),
SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: asks.publicKey,
lamports: await connection.getMinimumBalanceForRentExemption(65536 + 12),
space: 65536 + 12,
programId: dexProgram,
}),
DexInstructions.initializeMarket({
market: market.publicKey,
requestQueue: requestQueue.publicKey,
eventQueue: eventQueue.publicKey,
bids: bids.publicKey,
asks: asks.publicKey,
baseVault: baseVault.publicKey,
quoteVault: quoteVault.publicKey,
baseMint,
quoteMint,
baseLotSize: new anchor.BN(baseLotSize),
quoteLotSize: new anchor.BN(quoteLotSize),
feeRateBps,
vaultSignerNonce: vaultNonce,
quoteDustThreshold,
programId: dexProgram,
authority: undefined,
}),
);
const signedTransactions = await signTransactions({
transactionsAndSigners: [
{transaction: tx1, signers: [baseVault, quoteVault]},
{
transaction: tx2,
signers: [market, requestQueue, eventQueue, bids, asks],
},
],
wallet: wallet,
connection: connection,
});
for (let signedTransaction of signedTransactions) {
await sendSignedTransaction({
signedTransaction,
connection: connection,
});
}
return {
market: market.publicKey,
requestQueue: requestQueue.publicKey,
eventQueue: eventQueue.publicKey,
bids: bids.publicKey,
asks: asks.publicKey,
baseVault: baseVault.publicKey,
quoteVault: quoteVault.publicKey,
baseMint,
quoteMint,
baseLotSize: new anchor.BN(baseLotSize),
quoteLotSize: new anchor.BN(quoteLotSize),
feeRateBps,
vaultOwner,
vaultSignerNonce: vaultNonce,
quoteDustThreshold,
programId: dexProgram,
// authority: undefined,
};
}
export interface SerumMarketInfo {
market: PublicKey;
requestQueue: PublicKey;
eventQueue: PublicKey;
bids: PublicKey;
asks: PublicKey;
baseVault: PublicKey;
quoteVault: PublicKey;
baseMint: PublicKey;
quoteMint: PublicKey;
baseLotSize: anchor.BN;
quoteLotSize: anchor.BN;
feeRateBps: number;
vaultOwner: PublicKey;
vaultSignerNonce: anchor.BN;
quoteDustThreshold: any;
programId: any;
}
export async function getVaultOwnerAndNonce(marketId: PublicKey, dexProgramId: PublicKey) {
const vaultNonce = new anchor.BN(0);
while (true) {
try {
const vaultOwner = await PublicKey.createProgramAddress(
[marketId.toBuffer(), vaultNonce.toArrayLike(Buffer, 'le', 8)],
dexProgramId,
);
return {vaultOwner, vaultNonce};
} catch (e) {
vaultNonce.iaddn(1);
}
}
}
export async function signTransactions({
transactionsAndSigners,
wallet,
connection,
}: {
transactionsAndSigners: {
transaction: Transaction;
signers?: Array<Keypair>;
}[];
wallet: anchor.Wallet;
connection: Connection;
}) {
const blockhash = (await connection.getRecentBlockhash('max')).blockhash;
transactionsAndSigners.forEach(({transaction, signers = []}) => {
transaction.recentBlockhash = blockhash;
transaction.setSigners(
wallet.publicKey,
...signers.map((s) => s.publicKey),
);
if (signers?.length > 0) {
transaction.partialSign(...signers);
}
});
return await wallet.signAllTransactions(
transactionsAndSigners.map(({transaction}) => transaction),
);
}
export async function sendSignedTransaction({
signedTransaction,
connection,
timeout = 15000,
}: {
signedTransaction: Transaction;
connection: Connection;
timeout?: number;
}): Promise<string> {
const rawTransaction = signedTransaction.serialize();
const startTime = getUnixTs();
const txid: TransactionSignature = await connection.sendRawTransaction(
rawTransaction,
{
skipPreflight: true,
},
);
console.log('Started awaiting confirmation for', txid);
await sleep(timeout);
console.log('Latency', txid, getUnixTs() - startTime);
return txid;
}
export const getUnixTs = () => {
return new Date().getTime() / 1000;
};
export async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}