Skip to content

Commit

Permalink
Command-line tools to manage fee payer accounts and helper functions …
Browse files Browse the repository at this point in the history
…in payer-utils (anza-xyz#16)

* add CLI

* Refactor CLI and decouple fee payer utils to payer-utils in core

* restore original config

* add cli command to generate config for one token

* fix swap-tokens-to-sol cli command description

* use TokenFee in new endpoints as well
  • Loading branch information
sevazhidkov authored Sep 8, 2022
1 parent 2dbbe9c commit f06b842
Show file tree
Hide file tree
Showing 24 changed files with 964 additions and 75 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"test:live": "lerna run test:live",
"test:live-with-test-validator": "lerna run test:live-with-test-validator",
"lint": "lerna run --stream --no-bail lint",
"lint:fix": "lerna run --stream --no-bail lint:fix"
"lint:fix": "lerna run --stream --no-bail lint:fix",
"cli": "lerna run --stream cli --scope @solana/octane-server --no-prefix -- --"
},
"devDependencies": {
"lerna": "^5.4.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"@solana/web3.js": "^1.34.0",
"bs58": "^4.0.1",
"cache-manager": "^3.6.0",
"encoding": "^0.1.13"
"encoding": "^0.1.13",
"axios": "^0.27.2"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/createAccountIfTokenFeePaid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Connection, Keypair, Transaction } from '@solana/web3.js';
import {
AllowedToken,
TokenFee,
sha256,
simulateRawTransaction,
validateAccountInitializationInstructions,
Expand Down Expand Up @@ -31,7 +31,7 @@ export async function createAccountIfTokenFeePaid(
feePayer: Keypair,
maxSignatures: number,
lamportsPerSignature: number,
allowedTokens: AllowedToken[],
allowedTokens: TokenFee[],
cache: Cache,
sameSourceTimeout = 5000
) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/actions/signIfTokenFeePaid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
simulateRawTransaction,
validateTransaction,
validateTransfer,
AllowedToken,
TokenFee,
validateInstructions,
} from '../core';

Expand All @@ -30,7 +30,7 @@ export async function signWithTokenFee(
feePayer: Keypair,
maxSignatures: number,
lamportsPerSignature: number,
allowedTokens: AllowedToken[],
allowedTokens: TokenFee[],
cache: Cache,
sameSourceTimeout = 5000
): Promise<{ signature: string }> {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './clusters';
export * from './messageToken';
export * from './sha256';
export * from './simulateRawTransaction';
export * from './tokenFee';
export * from './validateAccountInitialization';
export * from './validateInstructions';
export * from './validateTransaction';
Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/core/tokenFee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { PublicKey } from '@solana/web3.js';

type SerializableTokenFee = {
mint: string;
account: string;
decimals: number;
fee: number;
}

export class TokenFee {
public mint: PublicKey;
public account: PublicKey;
public decimals: number;
public fee: bigint;

constructor(mint: PublicKey, account: PublicKey, decimals: number, fee: bigint) {
this.mint = mint;
this.account = account;
this.decimals = decimals;
this.fee = fee;
}

toSerializable(): SerializableTokenFee {
return {
mint: this.mint.toBase58(),
account: this.account.toBase58(),
decimals: this.decimals,
fee: Number(this.fee)
};
}

static fromSerializable(serializableToken: SerializableTokenFee): TokenFee {
return new TokenFee(
new PublicKey(serializableToken.mint),
new PublicKey(serializableToken.account),
serializableToken.decimals,
BigInt(serializableToken.fee)
);
}
}
12 changes: 3 additions & 9 deletions packages/core/src/core/validateTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,14 @@ import {
isTransferCheckedInstruction,
isTransferInstruction,
} from '@solana/spl-token';
import { Connection, PublicKey, Transaction } from '@solana/web3.js';

export type AllowedToken = {
mint: PublicKey;
account: PublicKey;
decimals: number; // Token account to receive fee payments
fee: bigint;
};
import { Connection, Transaction } from '@solana/web3.js';
import { TokenFee } from './tokenFee';

// Check that a transaction contains a valid transfer to Octane's token account
export async function validateTransfer(
connection: Connection,
transaction: Transaction,
allowedTokens: AllowedToken[]
allowedTokens: TokenFee[]
): Promise<DecodedTransferInstruction | DecodedTransferCheckedInstruction> {
// Get the first instruction of the transaction
const [first] = transaction.instructions;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './actions';
export * as core from './core';
export * as PayerUtils from './payer-utils';
export * from './swapProviders';
65 changes: 65 additions & 0 deletions packages/core/src/payer-utils/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { TokenFee } from '../core';
import { createAssociatedTokenAccount, getAssociatedTokenAddress } from '@solana/spl-token';

export type CreateAccount = {
address: PublicKey;
mint: PublicKey;
};

export type CreateAccountResult = {
address: PublicKey;
mint: PublicKey;
error: Error | null;
};

export async function buildCreateAccountListFromTokenFees(
connection: Connection,
feePayer: PublicKey,
tokenFees: TokenFee[]
): Promise<CreateAccount[]> {
let createAccounts: CreateAccount[] = [];
for (const tokenFee of tokenFees) {
const alreadyCreated = await connection.getAccountInfo(tokenFee.account);
if (alreadyCreated) {
continue;
}

const associatedWithFeePayer = tokenFee.account.equals(
await getAssociatedTokenAddress(tokenFee.mint, feePayer)
);
if (!associatedWithFeePayer) {
continue;
}

createAccounts.push({ mint: tokenFee.mint, address: tokenFee.account });
}

return createAccounts;
}

export async function createAccounts(
connection: Connection,
feePayer: Keypair,
accounts: CreateAccount[]
): Promise<CreateAccountResult[]> {
let results: CreateAccountResult[] = [];

for (const account of accounts) {
let error: Error | null = null;
try {
await createAssociatedTokenAccount(
connection,
feePayer,
account.mint,
feePayer.publicKey,
);
} catch (e) {
error = e as Error;
}

results.push({...account, error})
}

return results;
}
4 changes: 4 additions & 0 deletions packages/core/src/payer-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './accounts';
export * from './tokenFees';
export * from './jupiter';
export * from './swaps';
120 changes: 120 additions & 0 deletions packages/core/src/payer-utils/jupiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import axios from 'axios';
import { PublicKey, Transaction } from '@solana/web3.js';
import { NATIVE_MINT } from '@solana/spl-token';

export type TokenPriceInfo = {
id: string;
mintSymbol: string;
vsToken: string;
vsTokenSymbol: string;
price: number;
}

type TokenPriceInfoResponse = {
data: TokenPriceInfo;
timeTaken: number;
}

export type Route = {
inAmount: number;
outAmount: number;
amount: number;
otherAmountThreshold: number;
outAmountWithSlippage: number;
swapMode: string;
priceImpactPct: number;
marketInfos: RouteMarketInfo[];
}

export type RouteMarketInfo = {
id: string;
label: string;
inputMint: string;
outputMint: string;
inAmount: number;
outAmount: number;
lpFee: RouteFee;
platformFee: RouteFee;
notEnoughLiquidity: boolean;
priceImpactPct: number;
minInAmount?: number;
minOutAmount?: number;
}

export type RouteFee = {
amount: number;
mint: string;
pct: number;
}

type RoutesResponse = {
data: Route[];
timeTaken: number;
contextSlot: string;
}

export type SwapTransactions = {
setup: Transaction | null;
swap: Transaction | null;
cleanup: Transaction | null;
}

type SwapTransactionsResponse = {
setupTransaction: string | null;
swapTransaction: string | null;
cleanupTransaction: string | null;
}

export async function getPopularTokens(count: number, excludeNative = true): Promise<PublicKey[]> {
const response = await axios.get('https://cache.jup.ag/top-tokens');
const mints = response.data.map((mint: string) => new PublicKey(mint)) as PublicKey[];
const filteredMints = excludeNative ? mints.filter(value => !value.equals(NATIVE_MINT)) : mints;
return filteredMints.slice(0, count);
}

export async function getTokenToNativePriceInfo(mint: PublicKey): Promise<TokenPriceInfo> {
const priceInfoResponse = (
await axios.get('https://price.jup.ag/v1/price', {params: {id: 'SOL', vsToken: mint.toBase58()}})
).data as TokenPriceInfoResponse;
return priceInfoResponse.data;
}

export async function getRoutes(
inputMint: PublicKey,
outputMint: PublicKey,
amount: BigInt,
slippage: number
): Promise<Route[]> {
const params = {
inputMint: inputMint.toBase58(),
outputMint: outputMint.toBase58(),
amount: amount,
slippage: slippage,
};
const routesResponse = (await axios.get(
'https://quote-api.jup.ag/v1/quote', { params }
)).data as RoutesResponse;
return routesResponse.data;
}

export async function getSwapTransactions(wallet: PublicKey, route: Route): Promise<SwapTransactions> {
const decodeTransactionOrNull = (serialized: string | null) => (
serialized !== null ? Transaction.from(Buffer.from(serialized, 'base64')) : null
);

const response = (
await axios.post('https://quote-api.jup.ag/v1/swap', {
route,
userPublicKey: wallet.toString(),
wrapUnwrapSOL: true,
}, {
headers: { 'Content-Type': 'application/json' }
})
).data as SwapTransactionsResponse;
return {
setup: decodeTransactionOrNull(response.setupTransaction),
swap: decodeTransactionOrNull(response.swapTransaction),
cleanup: decodeTransactionOrNull(response.cleanupTransaction),
}
}

45 changes: 45 additions & 0 deletions packages/core/src/payer-utils/swaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Connection, Keypair } from '@solana/web3.js';
import { getAccount, NATIVE_MINT } from '@solana/spl-token';
import { TokenFee } from '../core';
import { getRoutes, getSwapTransactions, Route } from './jupiter';

export async function loadSwapRoutesForTokenFees(
connection: Connection,
tokenFees: TokenFee[],
thresholdInLamports: number,
slippage: number = 0.5
): Promise<Route[]> {
let routes = [];
for (const tokenFee of tokenFees) {
const account = await getAccount(connection, tokenFee.account);
if (account.amount === 0n) {
continue;
}
const route = (await getRoutes(
tokenFee.mint, NATIVE_MINT, account.amount, slippage
))[0];
if (route.outAmount < thresholdInLamports) {
continue;
}
routes.push(route);
}
return routes;
}

export async function executeSwapByRoute(connection: Connection, feePayer: Keypair, route: Route): Promise<string[]> {
const transactions = await getSwapTransactions(feePayer.publicKey, route);
let txids = [];
for (const transaction of [transactions.setup, transactions.swap, transactions.cleanup]) {
if (transaction === null) {
continue;
}
const txid = await connection.sendTransaction(
transaction,
[feePayer],
{ skipPreflight: true }
);
await connection.confirmTransaction(txid);
txids.push(txid);
}
return txids;
}
Loading

0 comments on commit f06b842

Please sign in to comment.