Merge branch 'main' into feat/squads_multisig

This commit is contained in:
A91y
2025-01-08 22:58:50 +05:30
65 changed files with 10161 additions and 320 deletions

View File

@@ -0,0 +1,71 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
import { closeEmptyTokenAccounts } from "../tools";
const closeEmptyTokenAccountsAction: Action = {
name: "CLOSE_EMPTY_TOKEN_ACCOUNTS",
similes: [
"close token accounts",
"remove empty accounts",
"clean up token accounts",
"close SPL token accounts",
"clean wallet",
],
description: `Close empty SPL Token accounts associated with your wallet to reclaim rent.
This action will close both regular SPL Token accounts and Token-2022 accounts that have zero balance. `,
examples: [
[
{
input: {},
output: {
status: "success",
signature:
"3KmPyiZvJQk8CfBVVaz8nf3c2crb6iqjQVDqNxknnusyb1FTFpXqD8zVSCBAd1X3rUcD8WiG1bdSjFbeHsmcYGXY",
accountsClosed: 10,
},
explanation: "Closed 10 empty token accounts successfully.",
},
],
[
{
input: {},
output: {
status: "success",
signature: "",
accountsClosed: 0,
},
explanation: "No empty token accounts were found to close.",
},
],
],
schema: z.object({}),
handler: async (agent: SolanaAgentKit) => {
try {
const result = await closeEmptyTokenAccounts(agent);
if (result.size === 0) {
return {
status: "success",
signature: "",
accountsClosed: 0,
message: "No empty token accounts found to close",
};
}
return {
status: "success",
signature: result.signature,
accountsClosed: result.size,
message: `Successfully closed ${result.size} empty token accounts`,
};
} catch (error: any) {
return {
status: "error",
message: `Failed to close empty token accounts: ${error.message}`,
};
}
},
};
export default closeEmptyTokenAccountsAction;

View File

@@ -0,0 +1,68 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
import { flashCloseTrade } from "../tools";
const flashCloseTradeAction: Action = {
name: "FLASH_CLOSE_TRADE",
similes: [
"close trade",
"close leveraged trade",
"exit position",
"close position",
"exit trade",
"close long",
"close short",
"take profit",
"stop loss",
],
description:
"Close an existing leveraged trading position on Flash.Trade protocol",
examples: [
[
{
input: {
token: "SOL",
side: "long",
},
output: {
status: "success",
signature: "4xKpN2...",
message: "Successfully closed long position on SOL",
},
explanation: "Close an existing long position on SOL",
},
],
],
schema: z.object({
token: z
.string()
.describe("Token symbol of the position to close (e.g. SOL, ETH)"),
side: z
.enum(["long", "short"])
.describe("Position side to close - long or short"),
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
try {
const params = {
token: input.token as string,
side: input.side as "long" | "short",
};
const response = await flashCloseTrade(agent, params);
return {
status: "success",
signature: response,
message: `Successfully closed ${params.side} position on ${params.token}`,
};
} catch (error: any) {
return {
status: "error",
message: `Flash trade close failed: ${error.message}`,
};
}
},
};
export default flashCloseTradeAction;

View File

@@ -0,0 +1,78 @@
import { Action } from "../types/action";
import { SolanaAgentKit } from "../agent";
import { z } from "zod";
import { flashOpenTrade } from "../tools";
const flashOpenTradeAction: Action = {
name: "FLASH_OPEN_TRADE",
similes: [
"open trade",
"open leveraged trade",
"start trading position",
"open position",
"long position",
"short position",
"leverage trade",
"margin trade",
],
description: "Open a leveraged trading position on Flash.Trade protocol",
examples: [
[
{
input: {
token: "SOL",
side: "long",
collateralUsd: 100,
leverage: 5,
},
output: {
status: "success",
signature: "4xKpN2...",
message:
"Successfully opened 5x long position on SOL with $100 collateral",
},
explanation:
"Open a 5x leveraged long position on SOL using $100 as collateral",
},
],
],
schema: z.object({
token: z.string().describe("Token symbol to trade (e.g. SOL, ETH)"),
side: z
.enum(["long", "short"])
.describe("Trading direction - long or short"),
collateralUsd: z
.number()
.positive()
.describe("Amount of collateral in USD"),
leverage: z
.number()
.positive()
.describe("Leverage multiplier for the trade"),
}),
handler: async (agent: SolanaAgentKit, input: Record<string, any>) => {
try {
const params = {
token: input.token as string,
side: input.side as "long" | "short",
collateralUsd: input.collateralUsd as number,
leverage: input.leverage as number,
};
const response = await flashOpenTrade(agent, params);
return {
status: "success",
signature: response,
message: `Successfully opened ${params.leverage}x ${params.side} position on ${params.token} with $${params.collateralUsd} collateral`,
};
} catch (error: any) {
return {
status: "error",
message: `Flash trade failed: ${error.message}`,
};
}
},
};
export default flashOpenTradeAction;

View File

@@ -28,6 +28,8 @@ import raydiumCreateAmmV4Action from "./raydiumCreateAmmV4";
import createOrcaSingleSidedWhirlpoolAction from "./createOrcaSingleSidedWhirlpool";
import launchPumpfunTokenAction from "./launchPumpfunToken";
import getWalletAddressAction from "./getWalletAddress";
import flashOpenTradeAction from "./flashOpenTrade";
import flashCloseTradeAction from "./flashCloseTrade";
export const ACTIONS = {
WALLET_ADDRESS_ACTION: getWalletAddressAction,
@@ -61,6 +63,8 @@ export const ACTIONS = {
CREATE_ORCA_SINGLE_SIDED_WHIRLPOOL_ACTION:
createOrcaSingleSidedWhirlpoolAction,
LAUNCH_PUMPFUN_TOKEN_ACTION: launchPumpfunTokenAction,
FLASH_OPEN_TRADE_ACTION: flashOpenTradeAction,
FLASH_CLOSE_TRADE_ACTION: flashCloseTradeAction,
};
export type { Action, ActionExample, Handler } from "../types/action";

View File

@@ -56,10 +56,13 @@ import {
create_TipLink,
listNFTForSale,
cancelListing,
closeEmptyTokenAccounts,
fetchTokenReportSummary,
fetchTokenDetailedReport,
fetchPythPrice,
fetchPythPriceFeedID,
flashOpenTrade,
flashCloseTrade,
} from "../tools";
import {
CollectionDeployment,
@@ -70,7 +73,18 @@ import {
PumpfunLaunchResponse,
PumpFunTokenOptions,
OrderParams,
FlashTradeParams,
FlashCloseTradeParams,
} from "../types";
import {
createCollection,
createSingle,
} from "../tools/create_3land_collectible";
import {
CreateCollectionOptions,
CreateSingleOptions,
StoreInitOptions,
} from "@3land/listings-sdk/dist/types/implementation/implementationTypes";
import { create_squads_multisig } from "../tools/squads_multisig/create_multisig";
import { deposit_to_multisig } from "../tools/squads_multisig/deposit_to_multisig";
import { transfer_from_multisig } from "../tools/squads_multisig/transfer_from_multisig";
@@ -541,6 +555,13 @@ export class SolanaAgentKit {
return cancelListing(this, nftMint);
}
async closeEmptyTokenAccounts(): Promise<{
signature: string;
size: number;
}> {
return closeEmptyTokenAccounts(this);
}
async fetchTokenReportSummary(mint: string): Promise<TokenCheck> {
return fetchTokenReportSummary(mint);
}
@@ -549,6 +570,47 @@ export class SolanaAgentKit {
return fetchTokenDetailedReport(mint);
}
/**
* Opens a new trading position on Flash.Trade
* @param params Flash trade parameters including market, side, collateral, leverage, and pool name
* @returns Transaction signature
*/
async flashOpenTrade(params: FlashTradeParams): Promise<string> {
return flashOpenTrade(this, params);
}
/**
* Closes an existing trading position on Flash.Trade
* @param params Flash trade close parameters
* @returns Transaction signature
*/
async flashCloseTrade(params: FlashCloseTradeParams): Promise<string> {
return flashCloseTrade(this, params);
}
async create3LandCollection(
optionsWithBase58: StoreInitOptions,
collectionOpts: CreateCollectionOptions,
): Promise<string> {
const tx = await createCollection(optionsWithBase58, collectionOpts);
return `Transaction: ${tx}`;
}
async create3LandNft(
optionsWithBase58: StoreInitOptions,
collectionAccount: string,
createItemOptions: CreateSingleOptions,
isMainnet: boolean,
): Promise<string> {
const tx = await createSingle(
optionsWithBase58,
collectionAccount,
createItemOptions,
isMainnet,
);
return `Transaction: ${tx}`;
}
async createSquadsMultisig(creator: PublicKey): Promise<string> {
return create_squads_multisig(this, creator);
}

View File

@@ -9,6 +9,12 @@ import {
SolanaAgentKit,
} from "../index";
import { create_image, FEE_TIERS, generateOrdersfromPattern } from "../tools";
import { marketTokenMap } from "../utils/flashUtils";
import {
CreateCollectionOptions,
CreateSingleOptions,
StoreInitOptions,
} from "@3land/listings-sdk/dist/types/implementation/implementationTypes";
export class SolanaBalanceTool extends Tool {
name = "solana_balance";
@@ -774,6 +780,135 @@ export class SolanaGetWalletAddressTool extends Tool {
}
}
export class SolanaFlashOpenTrade extends Tool {
name = "solana_flash_open_trade";
description = `This tool can be used to open a new leveraged trading position on Flash.Trade exchange.
Inputs ( input is a JSON string ):
token: string, eg "SOL", "BTC", "ETH" (required)
type: string, eg "long", "short" (required)
collateral: number, eg 10, 100, 1000 (required)
leverage: number, eg 5, 10, 20 (required)
Example prompt is Open a 20x leveraged trade for SOL on long side using flash trade with 500 USD as collateral`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input);
// Validate input parameters
if (!parsedInput.token) {
throw new Error("Token is required, received: " + parsedInput.token);
}
if (!Object.keys(marketTokenMap).includes(parsedInput.token)) {
throw new Error(
"Token must be one of " +
Object.keys(marketTokenMap).join(", ") +
", received: " +
parsedInput.token +
"\n" +
"Please check https://beast.flash.trade/ for the list of supported tokens",
);
}
if (!["long", "short"].includes(parsedInput.type)) {
throw new Error(
'Type must be either "long" or "short", received: ' +
parsedInput.type,
);
}
if (!parsedInput.collateral || parsedInput.collateral <= 0) {
throw new Error(
"Collateral amount must be positive, received: " +
parsedInput.collateral,
);
}
if (!parsedInput.leverage || parsedInput.leverage <= 0) {
throw new Error(
"Leverage must be positive, received: " + parsedInput.leverage,
);
}
const tx = await this.solanaKit.flashOpenTrade({
token: parsedInput.token,
side: parsedInput.type,
collateralUsd: parsedInput.collateral,
leverage: parsedInput.leverage,
});
return JSON.stringify({
status: "success",
message: "Flash trade position opened successfully",
transaction: tx,
token: parsedInput.token,
side: parsedInput.type,
collateral: parsedInput.collateral,
leverage: parsedInput.leverage,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaFlashCloseTrade extends Tool {
name = "solana_flash_close_trade";
description = `Close an existing leveraged trading position on Flash.Trade exchange.
Inputs ( input is a JSON string ):
token: string, eg "SOL", "BTC", "ETH" (required)
side: string, eg "long", "short" (required)
Example prompt is Close a 20x leveraged trade for SOL on long side`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const parsedInput = JSON.parse(input);
// Validate input parameters
if (!parsedInput.token) {
throw new Error("Token is required");
}
if (!["SOL", "BTC", "ETH"].includes(parsedInput.token)) {
throw new Error('Token must be one of ["SOL", "BTC", "ETH"]');
}
if (!["long", "short"].includes(parsedInput.side)) {
throw new Error('Side must be either "long" or "short"');
}
const tx = await this.solanaKit.flashCloseTrade({
token: parsedInput.token,
side: parsedInput.side,
});
return JSON.stringify({
status: "success",
message: "Flash trade position closed successfully",
transaction: tx,
token: parsedInput.token,
side: parsedInput.side,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaPumpfunTokenLaunchTool extends Tool {
name = "solana_launch_pumpfun_token";
@@ -2125,6 +2260,181 @@ export class SolanaFetchTokenDetailedReportTool extends Tool {
}
}
export class Solana3LandCreateSingle extends Tool {
name = "3land_minting_tool";
description = `Creates an NFT and lists it on 3.land's website
Inputs:
privateKey (required): represents the privateKey of the wallet - can be an array of numbers, Uint8Array or base58 string
collectionAccount (optional): represents the account for the nft collection
itemName (required): the name of the NFT
sellerFee (required): the fee of the seller
itemAmount (required): the amount of the NFTs that can be minted
itemDescription (required): the description of the NFT
traits (required): the traits of the NFT [{trait_type: string, value: string}]
price (required): the price of the item, if is 0 the listing will be free
mainImageUrl (required): the main image of the NFT
coverImageUrl (optional): the cover image of the NFT
splHash (optional): the hash of the spl token, if not provided listing will be in $SOL
isMainnet (required): defines is the tx takes places in mainnet
`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const privateKey = inputFormat.privateKey;
const isMainnet = inputFormat.isMainnet;
const optionsWithBase58: StoreInitOptions = {
...(privateKey && { privateKey }),
...(isMainnet && { isMainnet }),
};
const collectionAccount = inputFormat.collectionAccount;
const itemName = inputFormat?.itemName;
const sellerFee = inputFormat?.sellerFee;
const itemAmount = inputFormat?.itemAmount;
const itemSymbol = inputFormat?.itemSymbol;
const itemDescription = inputFormat?.itemDescription;
const traits = inputFormat?.traits;
const price = inputFormat?.price;
const mainImageUrl = inputFormat?.mainImageUrl;
const coverImageUrl = inputFormat?.coverImageUrl;
const splHash = inputFormat?.splHash;
const createItemOptions: CreateSingleOptions = {
...(itemName && { itemName }),
...(sellerFee && { sellerFee }),
...(itemAmount && { itemAmount }),
...(itemSymbol && { itemSymbol }),
...(itemDescription && { itemDescription }),
...(traits && { traits }),
...(price && { price }),
...(mainImageUrl && { mainImageUrl }),
...(coverImageUrl && { coverImageUrl }),
...(splHash && { splHash }),
};
if (!collectionAccount) {
throw new Error("Collection account is required");
}
const tx = await this.solanaKit.create3LandNft(
optionsWithBase58,
collectionAccount,
createItemOptions,
isMainnet,
);
return JSON.stringify({
status: "success",
message: `Created listing successfully ${tx}`,
transaction: tx,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class Solana3LandCreateCollection extends Tool {
name = "3land_minting_tool";
description = `Creates an NFT Collection that you can visit on 3.land's website (3.land/collection/{collectionAccount})
Inputs:
privateKey (required): represents the privateKey of the wallet - can be an array of numbers, Uint8Array or base58 string
isMainnet (required): defines is the tx takes places in mainnet
collectionSymbol (required): the symbol of the collection
collectionName (required): the name of the collection
collectionDescription (required): the description of the collection
mainImageUrl (required): the image of the collection
coverImageUrl (optional): the cover image of the collection
`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(input: string): Promise<string> {
try {
const inputFormat = JSON.parse(input);
const privateKey = inputFormat.privateKey;
const isMainnet = inputFormat.isMainnet;
const optionsWithBase58: StoreInitOptions = {
...(privateKey && { privateKey }),
...(isMainnet && { isMainnet }),
};
const collectionSymbol = inputFormat?.collectionSymbol;
const collectionName = inputFormat?.collectionName;
const collectionDescription = inputFormat?.collectionDescription;
const mainImageUrl = inputFormat?.mainImageUrl;
const coverImageUrl = inputFormat?.coverImageUrl;
const collectionOpts: CreateCollectionOptions = {
...(collectionSymbol && { collectionSymbol }),
...(collectionName && { collectionName }),
...(collectionDescription && { collectionDescription }),
...(mainImageUrl && { mainImageUrl }),
...(coverImageUrl && { coverImageUrl }),
};
const tx = await this.solanaKit.create3LandCollection(
optionsWithBase58,
collectionOpts,
);
return JSON.stringify({
status: "success",
message: `Created Collection successfully ${tx}`,
transaction: tx,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaCloseEmptyTokenAccounts extends Tool {
name = "close_empty_token_accounts";
description = `Close all empty spl-token accounts and reclaim the rent`;
constructor(private solanaKit: SolanaAgentKit) {
super();
}
protected async _call(): Promise<string> {
try {
const { signature, size } =
await this.solanaKit.closeEmptyTokenAccounts();
return JSON.stringify({
status: "success",
message: `${size} accounts closed successfully. ${size === 48 ? "48 accounts can be closed in a single transaction try again to close more accounts" : ""}`,
signature,
});
} catch (error: any) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR",
});
}
}
}
export class SolanaCreate2by2Multisig extends Tool {
name = "create_2by2_multisig";
description = `Create a 2-of-2 multisig account on Solana with the user and the agent, where both approvals will be required to run the transactions.
@@ -2428,10 +2738,16 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
new SolanaTipLinkTool(solanaKit),
new SolanaListNFTForSaleTool(solanaKit),
new SolanaCancelNFTListingTool(solanaKit),
new SolanaCloseEmptyTokenAccounts(solanaKit),
new SolanaFetchTokenReportSummaryTool(solanaKit),
new SolanaFetchTokenDetailedReportTool(solanaKit),
new Solana3LandCreateSingle(solanaKit),
new Solana3LandCreateCollection(solanaKit),
new SolanaPerpOpenTradeTool(solanaKit),
new SolanaPerpCloseTradeTool(solanaKit),
new SolanaFlashOpenTrade(solanaKit),
new SolanaFlashCloseTrade(solanaKit),
new Solana3LandCreateSingle(solanaKit),
new SolanaCreate2by2Multisig(solanaKit),
new SolanaDepositTo2by2Multisig(solanaKit),
new SolanaTransferFrom2by2Multisig(solanaKit),

View File

@@ -1,37 +0,0 @@
import {
PublicKey,
sendAndConfirmTransaction,
Transaction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
import { ManifestClient } from "@cks-systems/manifest-sdk";
/**
* Cancels all orders from Manifest
* @param agent SolanaAgentKit instance
* @param marketId Public key for the manifest market
* @returns Transaction signature
*/
export async function cancelAllOrders(
agent: SolanaAgentKit,
marketId: PublicKey,
): Promise<string> {
try {
const mfxClient = await ManifestClient.getClientForMarket(
agent.connection,
marketId,
agent.wallet,
);
const cancelAllOrdersIx = await mfxClient.cancelAllIx();
const signature = await sendAndConfirmTransaction(
agent.connection,
new Transaction().add(cancelAllOrdersIx),
[agent.wallet],
);
return signature;
} catch (error: any) {
throw new Error(`Cancel all orders failed: ${error.message}`);
}
}

View File

@@ -0,0 +1,103 @@
import {
PublicKey,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../agent";
import {
AccountLayout,
createCloseAccountInstruction,
TOKEN_2022_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
/**
* Close Empty SPL Token accounts of the agent
* @param agent SolanaAgentKit instance
* @returns transaction signature and total number of accounts closed
*/
export async function closeEmptyTokenAccounts(
agent: SolanaAgentKit,
): Promise<{ signature: string; size: number }> {
try {
const spl_token = await create_close_instruction(agent, TOKEN_PROGRAM_ID);
const token_2022 = await create_close_instruction(
agent,
TOKEN_2022_PROGRAM_ID,
);
const transaction = new Transaction();
const MAX_INSTRUCTIONS = 40; // 40 instructions can be processed in a single transaction without failing
spl_token
.slice(0, Math.min(MAX_INSTRUCTIONS, spl_token.length))
.forEach((instruction) => transaction.add(instruction));
token_2022
.slice(0, Math.max(0, MAX_INSTRUCTIONS - spl_token.length))
.forEach((instruction) => transaction.add(instruction));
const size = spl_token.length + token_2022.length;
if (size === 0) {
return {
signature: "",
size: 0,
};
}
const signature = await agent.connection.sendTransaction(transaction, [
agent.wallet,
]);
return { signature, size };
} catch (error) {
throw new Error(`Error closing empty token accounts: ${error}`);
}
}
/**
* creates the close instuctions of a spl token account
* @param agnet SolanaAgentKit instance
* @param token_program Token Program Id
* @returns close instuction array
*/
async function create_close_instruction(
agent: SolanaAgentKit,
token_program: PublicKey,
): Promise<TransactionInstruction[]> {
const instructions = [];
const ata_accounts = await agent.connection.getTokenAccountsByOwner(
agent.wallet_address,
{ programId: token_program },
"confirmed",
);
const tokens = ata_accounts.value;
const accountExceptions = [
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC
];
for (let i = 0; i < tokens.length; i++) {
const token_data = AccountLayout.decode(tokens[i].account.data);
if (
token_data.amount === BigInt(0) &&
!accountExceptions.includes(token_data.mint.toString())
) {
const closeInstruction = createCloseAccountInstruction(
ata_accounts.value[i].pubkey,
agent.wallet_address,
agent.wallet_address,
[],
token_program,
);
instructions.push(closeInstruction);
}
}
return instructions;
}

View File

@@ -0,0 +1,69 @@
import { createCollectionImp, createSingleImp } from "@3land/listings-sdk";
import {
StoreInitOptions,
CreateCollectionOptions,
CreateSingleOptions,
} from "@3land/listings-sdk/dist/types/implementation/implementationTypes";
/**
* Create a collection on 3Land
* @param optionsWithBase58 represents the privateKey of the wallet - can be an array of numbers, Uint8Array or base58 string
* @param collectionOpts represents the options for the collection creation
* @returns
*/
export async function createCollection(
optionsWithBase58: StoreInitOptions,
collectionOpts: CreateCollectionOptions,
) {
try {
const collection = await createCollectionImp(
optionsWithBase58,
collectionOpts,
);
return collection;
} catch (error: any) {
throw new Error(`Collection creation failed: ${error.message}`);
}
}
/**
* Create a single edition on 3Land
* @param optionsWithBase58 represents the privateKey of the wallet - can be an array of numbers, Uint8Array or base58 string
* @param collectionAccount represents the account for the nft collection
* @param createItemOptions the options for the creation of the single NFT listing
* @returns
*/
export async function createSingle(
optionsWithBase58: StoreInitOptions,
collectionAccount: string,
createItemOptions: CreateSingleOptions,
isMainnet: boolean,
) {
try {
const landStore = isMainnet
? "AmQNs2kgw4LvS9sm6yE9JJ4Hs3JpVu65eyx9pxMG2xA"
: "GyPCu89S63P9NcCQAtuSJesiefhhgpGWrNVJs4bF2cSK";
const singleEditionTx = await createSingleImp(
optionsWithBase58,
landStore,
collectionAccount,
createItemOptions,
);
return singleEditionTx;
} catch (error: any) {
throw new Error(`Single edition creation failed: ${error.message}`);
}
}
/**
* Buy a single edition on 3Land
* @param
* @returns
*/
// export async function buySingle() {
// try {
// } catch (error: any) {
// throw new Error(`Buying single edition failed: ${error.message}`);
// }
// }

View File

@@ -0,0 +1,118 @@
import { ComputeBudgetProgram } from "@solana/web3.js";
import { PoolConfig, Side } from "flash-sdk";
import { BN } from "@coral-xyz/anchor";
import { SolanaAgentKit } from "../index";
import {
CLOSE_POSITION_CU,
marketSdkInfo,
marketTokenMap,
getNftTradingAccountInfo,
fetchOraclePrice,
createPerpClient,
get_flash_privilege,
} from "../utils/flashUtils";
import { FlashCloseTradeParams } from "../types";
/**
* Closes an existing position on Flash.Trade
* @param agent SolanaAgentKit instance
* @param params Trade parameters
* @returns Transaction signature
*/
export async function flashCloseTrade(
agent: SolanaAgentKit,
params: FlashCloseTradeParams,
): Promise<string> {
try {
const { token, side } = params;
// Get market ID from token and side using marketTokenMap
const tokenMarkets = marketTokenMap[token];
if (!tokenMarkets) {
throw new Error(`Token ${token} not supported for trading`);
}
const sideEntry = tokenMarkets[side];
if (!sideEntry) {
throw new Error(`${side} side not available for ${token}`);
}
const market = sideEntry.marketID;
// Validate market data using marketSdkInfo
const marketData = marketSdkInfo[market];
if (!marketData) {
throw new Error(`Invalid market configuration for ${token}/${side}`);
}
// Get token information
const [targetSymbol, collateralSymbol] = marketData.tokenPair.split("/");
// Fetch oracle prices
const [targetPrice] = await Promise.all([
fetchOraclePrice(targetSymbol),
fetchOraclePrice(collateralSymbol),
]);
// Initialize pool configuration and perpClient
const poolConfig = PoolConfig.fromIdsByName(
marketData.pool,
"mainnet-beta",
);
const perpClient = createPerpClient(agent.connection, agent.wallet);
// Calculate price after slippage
const slippageBpsBN = new BN(100); // 1% slippage
const sideEnum = side === "long" ? Side.Long : Side.Short;
const priceWithSlippage = perpClient.getPriceAfterSlippage(
false, // isEntry = false for closing position
slippageBpsBN,
targetPrice.price,
sideEnum,
);
// Get NFT trading account info
const tradingAccounts = await getNftTradingAccountInfo(
agent.wallet_address,
perpClient,
poolConfig,
collateralSymbol,
);
if (
!tradingAccounts.nftTradingAccountPk ||
!tradingAccounts.nftReferralAccountPK ||
!tradingAccounts.nftOwnerRebateTokenAccountPk
) {
throw new Error("Required NFT trading accounts not found");
}
// Build and send transaction
const { instructions, additionalSigners } = await perpClient.closePosition(
targetSymbol,
collateralSymbol,
priceWithSlippage,
sideEnum,
poolConfig,
get_flash_privilege(agent),
tradingAccounts.nftTradingAccountPk,
tradingAccounts.nftReferralAccountPK,
tradingAccounts.nftOwnerRebateTokenAccountPk,
);
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: CLOSE_POSITION_CU,
});
return await perpClient.sendTransaction(
[computeBudgetIx, ...instructions],
{
additionalSigners: additionalSigners,
alts: perpClient.addressLookupTables,
prioritizationFee: 5000000,
},
);
} catch (error) {
throw new Error(`Flash trade close failed: ${error}`);
}
}

View File

@@ -0,0 +1,251 @@
import { ComputeBudgetProgram } from "@solana/web3.js";
import {
PerpetualsClient,
OraclePrice,
PoolConfig,
Side,
CustodyAccount,
Custody,
} from "flash-sdk";
import { BN } from "@coral-xyz/anchor";
import { SolanaAgentKit } from "../index";
import {
ALL_TOKENS,
marketSdkInfo,
marketTokenMap,
getNftTradingAccountInfo,
OPEN_POSITION_CU,
fetchOraclePrice,
createPerpClient,
get_flash_privilege,
} from "../utils/flashUtils";
import { FlashTradeParams } from "../types";
/**
* Opens a new position on Flash.Trade
* @param agent SolanaAgentKit instance
* @param params Trade parameters
* @returns Transaction signature
*/
export async function flashOpenTrade(
agent: SolanaAgentKit,
params: FlashTradeParams,
): Promise<string> {
try {
const { token, side, collateralUsd, leverage } = params;
// Get market ID from token and side using marketTokenMap
const tokenMarkets = marketTokenMap[token];
if (!tokenMarkets) {
throw new Error(`Token ${token} not supported for trading`);
}
const sideEntry = tokenMarkets[side];
if (!sideEntry) {
throw new Error(`${side} side not available for ${token}`);
}
const market = sideEntry.marketID;
// Validate market data using marketSdkInfo
const marketData = marketSdkInfo[market];
if (!marketData) {
throw new Error(`Invalid market configuration for ${token}/${side}`);
}
// Get token information
const [targetSymbol, collateralSymbol] = marketData.tokenPair.split("/");
const targetToken = ALL_TOKENS.find((t) => t.symbol === targetSymbol);
const collateralToken = ALL_TOKENS.find(
(t) => t.symbol === collateralSymbol,
);
if (!targetToken || !collateralToken) {
throw new Error(`Token not found for pair ${marketData.tokenPair}`);
}
// Fetch oracle prices
const [targetPrice, collateralPrice] = await Promise.all([
fetchOraclePrice(targetSymbol),
fetchOraclePrice(collateralSymbol),
]);
// Initialize pool configuration and perpClient
const poolConfig = PoolConfig.fromIdsByName(
marketData.pool,
"mainnet-beta",
);
const perpClient = createPerpClient(agent.connection, agent.wallet);
// Calculate position parameters
const leverageBN = new BN(leverage);
const collateralTokenPrice = convertPriceToNumber(collateralPrice.price);
const collateralAmount = calculateCollateralAmount(
collateralUsd,
collateralTokenPrice,
collateralToken.decimals,
);
// Get custody accounts
const { targetCustody, collateralCustody } = await fetchCustodyAccounts(
perpClient,
poolConfig,
targetSymbol,
collateralSymbol,
);
// Calculate position size
const positionSize = calculatePositionSize(
perpClient,
collateralAmount,
leverageBN,
targetToken,
collateralToken,
side,
targetPrice.price,
collateralPrice.price,
targetCustody,
collateralCustody,
);
// Get NFT trading account info
const tradingAccounts = await getNftTradingAccountInfo(
agent.wallet_address,
perpClient,
poolConfig,
collateralSymbol,
);
if (
!tradingAccounts.nftTradingAccountPk ||
!tradingAccounts.nftReferralAccountPK
) {
throw new Error("Required NFT trading accounts not found");
}
// Prepare transaction
const slippageBps = new BN(1000);
const priceWithSlippage = perpClient.getPriceAfterSlippage(
true,
slippageBps,
targetPrice.price,
side === "long" ? Side.Long : Side.Short,
);
// Build and send transaction
const { instructions, additionalSigners } = await perpClient.openPosition(
targetSymbol,
collateralSymbol,
priceWithSlippage,
collateralAmount,
positionSize,
side === "long" ? Side.Long : Side.Short,
poolConfig,
get_flash_privilege(agent),
tradingAccounts.nftTradingAccountPk,
tradingAccounts.nftReferralAccountPK,
tradingAccounts.nftOwnerRebateTokenAccountPk!,
false,
);
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitLimit({
units: OPEN_POSITION_CU,
});
return await perpClient.sendTransaction(
[computeBudgetIx, ...instructions],
{
additionalSigners: additionalSigners,
alts: perpClient.addressLookupTables,
prioritizationFee: 5000000,
},
);
} catch (error) {
throw new Error(`Flash trade failed: ${error}`);
}
}
// Helper functions
function convertPriceToNumber(oraclePrice: OraclePrice): number {
const price = parseInt(oraclePrice.price.toString("hex"), 16);
const exponent = parseInt(oraclePrice.exponent.toString("hex"), 16);
return price * Math.pow(10, exponent);
}
function calculateCollateralAmount(
usdAmount: number,
tokenPrice: number,
decimals: number,
): BN {
return new BN((usdAmount / tokenPrice) * Math.pow(10, decimals));
}
async function fetchCustodyAccounts(
perpClient: PerpetualsClient,
poolConfig: PoolConfig,
targetSymbol: string,
collateralSymbol: string,
) {
const targetConfig = poolConfig.custodies.find(
(c) => c.symbol === targetSymbol,
);
const collateralConfig = poolConfig.custodies.find(
(c) => c.symbol === collateralSymbol,
);
if (!targetConfig || !collateralConfig) {
throw new Error("Custody configuration not found");
}
const accounts = await perpClient.provider.connection.getMultipleAccountsInfo(
[targetConfig.custodyAccount, collateralConfig.custodyAccount],
);
if (!accounts[0] || !accounts[1]) {
throw new Error("Failed to fetch custody accounts");
}
return {
targetCustody: CustodyAccount.from(
targetConfig.custodyAccount,
perpClient.program.coder.accounts.decode<Custody>(
"custody",
accounts[0].data,
),
),
collateralCustody: CustodyAccount.from(
collateralConfig.custodyAccount,
perpClient.program.coder.accounts.decode<Custody>(
"custody",
accounts[1].data,
),
),
};
}
function calculatePositionSize(
perpClient: PerpetualsClient,
collateralAmount: BN,
leverage: BN,
targetToken: any,
collateralToken: any,
side: "long" | "short",
targetPrice: OraclePrice,
collateralPrice: OraclePrice,
targetCustody: CustodyAccount,
collateralCustody: CustodyAccount,
): BN {
return perpClient.getSizeAmountFromLeverageAndCollateral(
collateralAmount,
leverage.toString(),
targetToken,
collateralToken,
side === "long" ? Side.Long : Side.Short,
targetPrice,
targetPrice,
targetCustody,
collateralPrice,
collateralPrice,
collateralCustody,
);
}

View File

@@ -1,6 +1,4 @@
export * from "./adrena_perp_trading";
export * from "./batch_order";
export * from "./cancel_all_orders";
export * from "./create_gibwork_task";
export * from "./create_image";
export * from "./create_tiplinks";
@@ -20,8 +18,7 @@ export * from "./get_tps";
export * from "./get_wallet_address";
export * from "./launch_pumpfun_token";
export * from "./lend";
export * from "./limit_order";
export * from "./manifest_create_market";
export * from "./manifest_trade";
export * from "./mint_nft";
export * from "./openbook_create_market";
export * from "./orca_close_position";
@@ -44,6 +41,12 @@ export * from "./send_compressed_airdrop";
export * from "./stake_with_jup";
export * from "./stake_with_solayer";
export * from "./tensor_trade";
export * from "./close_empty_token_accounts";
export * from "./trade";
export * from "./transfer";
export * from "./withdraw_all";
export * from "./flash_open_trade";
export * from "./flash_close_trade";
export * from "./create_3land_collectible";

View File

@@ -1,61 +0,0 @@
import {
PublicKey,
Transaction,
sendAndConfirmTransaction,
TransactionInstruction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
import {
ManifestClient,
WrapperPlaceOrderParamsExternal,
OrderType,
} from "@cks-systems/manifest-sdk";
/**
* Place limit orders using Manifest
* @param agent SolanaAgentKit instance
* @param marketId Public key for the manifest market
* @param quantity Amount to trade in tokens
* @param side Buy or Sell
* @param price Price in tokens ie. SOL/USDC
* @returns Transaction signature
*/
export async function limitOrder(
agent: SolanaAgentKit,
marketId: PublicKey,
quantity: number,
side: string,
price: number,
): Promise<string> {
try {
const mfxClient = await ManifestClient.getClientForMarket(
agent.connection,
marketId,
agent.wallet,
);
const orderParams: WrapperPlaceOrderParamsExternal = {
numBaseTokens: quantity,
tokenPrice: price,
isBid: side === "Buy",
lastValidSlot: 0,
orderType: OrderType.Limit,
clientOrderId: Number(Math.random() * 1000),
};
const depositPlaceOrderIx: TransactionInstruction[] =
await mfxClient.placeOrderWithRequiredDepositIx(
agent.wallet.publicKey,
orderParams,
);
const signature = await sendAndConfirmTransaction(
agent.connection,
new Transaction().add(...depositPlaceOrderIx),
[agent.wallet],
);
return signature;
} catch (error: any) {
throw new Error(`Limit Order failed: ${error.message}`);
}
}

View File

@@ -1,43 +0,0 @@
import { ManifestClient } from "@cks-systems/manifest-sdk";
import {
Keypair,
PublicKey,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
export async function manifestCreateMarket(
agent: SolanaAgentKit,
baseMint: PublicKey,
quoteMint: PublicKey,
): Promise<string[]> {
const marketKeypair: Keypair = Keypair.generate();
const FIXED_MANIFEST_HEADER_SIZE: number = 256;
const createAccountIx: TransactionInstruction = SystemProgram.createAccount({
fromPubkey: agent.wallet.publicKey,
newAccountPubkey: marketKeypair.publicKey,
space: FIXED_MANIFEST_HEADER_SIZE,
lamports: await agent.connection.getMinimumBalanceForRentExemption(
FIXED_MANIFEST_HEADER_SIZE,
),
programId: new PublicKey("MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms"),
});
const createMarketIx = ManifestClient["createMarketIx"](
agent.wallet.publicKey,
baseMint,
quoteMint,
marketKeypair.publicKey,
);
const tx: Transaction = new Transaction();
tx.add(createAccountIx);
tx.add(createMarketIx);
const signature = await sendAndConfirmTransaction(agent.connection, tx, [
agent.wallet,
marketKeypair,
]);
return [signature, marketKeypair.publicKey.toBase58()];
}

View File

@@ -1,16 +1,159 @@
import {
PublicKey,
Transaction,
sendAndConfirmTransaction,
TransactionInstruction,
} from "@solana/web3.js";
import {
ManifestClient,
WrapperPlaceOrderParamsExternal,
OrderType,
WrapperPlaceOrderParamsExternal,
} from "@cks-systems/manifest-sdk";
import { SolanaAgentKit } from "../index";
import { BatchOrderPattern, OrderParams } from "../types";
import {
Keypair,
PublicKey,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import { BatchOrderPattern, OrderParams, SolanaAgentKit } from "../index";
export async function manifestCreateMarket(
agent: SolanaAgentKit,
baseMint: PublicKey,
quoteMint: PublicKey,
): Promise<string[]> {
const marketKeypair: Keypair = Keypair.generate();
const FIXED_MANIFEST_HEADER_SIZE: number = 256;
const createAccountIx: TransactionInstruction = SystemProgram.createAccount({
fromPubkey: agent.wallet.publicKey,
newAccountPubkey: marketKeypair.publicKey,
space: FIXED_MANIFEST_HEADER_SIZE,
lamports: await agent.connection.getMinimumBalanceForRentExemption(
FIXED_MANIFEST_HEADER_SIZE,
),
programId: new PublicKey("MNFSTqtC93rEfYHB6hF82sKdZpUDFWkViLByLd1k1Ms"),
});
const createMarketIx = ManifestClient["createMarketIx"](
agent.wallet.publicKey,
baseMint,
quoteMint,
marketKeypair.publicKey,
);
const tx: Transaction = new Transaction();
tx.add(createAccountIx);
tx.add(createMarketIx);
const signature = await sendAndConfirmTransaction(agent.connection, tx, [
agent.wallet,
marketKeypair,
]);
return [signature, marketKeypair.publicKey.toBase58()];
}
/**
* Place limit orders using Manifest
* @param agent SolanaAgentKit instance
* @param marketId Public key for the manifest market
* @param quantity Amount to trade in tokens
* @param side Buy or Sell
* @param price Price in tokens ie. SOL/USDC
* @returns Transaction signature
*/
export async function limitOrder(
agent: SolanaAgentKit,
marketId: PublicKey,
quantity: number,
side: string,
price: number,
): Promise<string> {
try {
const mfxClient = await ManifestClient.getClientForMarket(
agent.connection,
marketId,
agent.wallet,
);
const orderParams: WrapperPlaceOrderParamsExternal = {
numBaseTokens: quantity,
tokenPrice: price,
isBid: side === "Buy",
lastValidSlot: 0,
orderType: OrderType.Limit,
clientOrderId: Number(Math.random() * 1000),
};
const depositPlaceOrderIx: TransactionInstruction[] =
await mfxClient.placeOrderWithRequiredDepositIx(
agent.wallet.publicKey,
orderParams,
);
const signature = await sendAndConfirmTransaction(
agent.connection,
new Transaction().add(...depositPlaceOrderIx),
[agent.wallet],
);
return signature;
} catch (error: any) {
throw new Error(`Limit Order failed: ${error.message}`);
}
}
/**
* Cancels all orders from Manifest
* @param agent SolanaAgentKit instance
* @param marketId Public key for the manifest market
* @returns Transaction signature
*/
export async function cancelAllOrders(
agent: SolanaAgentKit,
marketId: PublicKey,
): Promise<string> {
try {
const mfxClient = await ManifestClient.getClientForMarket(
agent.connection,
marketId,
agent.wallet,
);
const cancelAllOrdersIx = await mfxClient.cancelAllIx();
const signature = await sendAndConfirmTransaction(
agent.connection,
new Transaction().add(cancelAllOrdersIx),
[agent.wallet],
);
return signature;
} catch (error: any) {
throw new Error(`Cancel all orders failed: ${error.message}`);
}
}
/**
* Withdraws all funds from Manifest
* @param agent SolanaAgentKit instance
* @param marketId Public key for the manifest market
* @returns Transaction signature
*/
export async function withdrawAll(
agent: SolanaAgentKit,
marketId: PublicKey,
): Promise<string> {
try {
const mfxClient = await ManifestClient.getClientForMarket(
agent.connection,
marketId,
agent.wallet,
);
const withdrawAllIx = await mfxClient.withdrawAllIx();
const signature = await sendAndConfirmTransaction(
agent.connection,
new Transaction().add(...withdrawAllIx),
[agent.wallet],
);
return signature;
} catch (error: any) {
throw new Error(`Withdraw all failed: ${error.message}`);
}
}
/**
* Generates an array of orders based on the specified pattern

View File

@@ -1,37 +0,0 @@
import {
PublicKey,
sendAndConfirmTransaction,
Transaction,
} from "@solana/web3.js";
import { SolanaAgentKit } from "../index";
import { ManifestClient } from "@cks-systems/manifest-sdk";
/**
* Withdraws all funds from Manifest
* @param agent SolanaAgentKit instance
* @param marketId Public key for the manifest market
* @returns Transaction signature
*/
export async function withdrawAll(
agent: SolanaAgentKit,
marketId: PublicKey,
): Promise<string> {
try {
const mfxClient = await ManifestClient.getClientForMarket(
agent.connection,
marketId,
agent.wallet,
);
const withdrawAllIx = await mfxClient.withdrawAllIx();
const signature = await sendAndConfirmTransaction(
agent.connection,
new Transaction().add(...withdrawAllIx),
[agent.wallet],
);
return signature;
} catch (error: any) {
throw new Error(`Withdraw all failed: ${error.message}`);
}
}

View File

@@ -6,6 +6,7 @@ export interface Config {
OPENAI_API_KEY?: string;
JUPITER_REFERRAL_ACCOUNT?: string;
JUPITER_FEE_BPS?: number;
FLASH_PRIVILEGE?: string;
}
export interface Creator {
@@ -224,3 +225,15 @@ export interface BatchOrderPattern {
numberOfOrders?: number;
individualQuantity?: number;
}
export interface FlashTradeParams {
token: string;
side: "long" | "short";
collateralUsd: number;
leverage: number;
}
export interface FlashCloseTradeParams {
token: string;
side: "long" | "short";
}

300
src/utils/flashUtils.ts Normal file
View File

@@ -0,0 +1,300 @@
import { HermesClient } from "@pythnetwork/hermes-client";
import { OraclePrice } from "flash-sdk";
import { AnchorProvider, BN, Wallet } from "@coral-xyz/anchor";
import {
PoolConfig,
Token,
Referral,
PerpetualsClient,
Privilege,
} from "flash-sdk";
import { Cluster, PublicKey, Connection, Keypair } from "@solana/web3.js";
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { SolanaAgentKit } from "../index";
const POOL_NAMES = [
"Crypto.1",
"Virtual.1",
"Governance.1",
"Community.1",
"Community.2",
"Community.3",
];
const DEFAULT_CLUSTER: Cluster = "mainnet-beta";
export const POOL_CONFIGS = POOL_NAMES.map((f) =>
PoolConfig.fromIdsByName(f, DEFAULT_CLUSTER),
);
const DUPLICATE_TOKENS = POOL_CONFIGS.map((f) => f.tokens).flat();
const tokenMap = new Map();
for (const token of DUPLICATE_TOKENS) {
tokenMap.set(token.symbol, token);
}
export const ALL_TOKENS: Token[] = Array.from(tokenMap.values());
export const ALL_CUSTODIES = POOL_CONFIGS.map((f) => f.custodies).flat();
const PROGRAM_ID = POOL_CONFIGS[0].programId;
// CU for trade instructions
export const OPEN_POSITION_CU = 150_000;
export const CLOSE_POSITION_CU = 180_000;
const HERMES_URL = "https://hermes.pyth.network"; // Replace with the actual Hermes URL if different
// Create a map of symbol to Pyth price ID
const PRICE_FEED_IDS = ALL_TOKENS.reduce(
(acc, token) => {
acc[token.symbol] = token.pythPriceId;
return acc;
},
{} as { [key: string]: string },
);
const hermesClient = new HermesClient(HERMES_URL, {});
export interface PythPriceEntry {
price: OraclePrice;
emaPrice: OraclePrice;
isStale: boolean;
status: PriceStatus;
}
export enum PriceStatus {
Trading,
Unknown,
Halted,
Auction,
}
export const fetchOraclePrice = async (
symbol: string,
): Promise<PythPriceEntry> => {
const priceFeedId = PRICE_FEED_IDS[symbol];
if (!priceFeedId) {
throw new Error(`Price feed ID not found for symbol: ${symbol}`);
}
try {
const hermesPriceFeed = await hermesClient.getPriceFeeds({
query: symbol,
filter: "crypto",
});
if (!hermesPriceFeed || hermesPriceFeed.length === 0) {
throw new Error(`No price feed received for ${symbol}`);
}
const hemrmesPriceUdpate = await hermesClient.getLatestPriceUpdates(
[priceFeedId],
{
encoding: "hex",
parsed: true,
},
);
if (!hemrmesPriceUdpate.parsed) {
throw new Error(`No price feed received for ${symbol}`);
}
const hermesEma = hemrmesPriceUdpate.parsed[0].ema_price;
const hermesPrice = hemrmesPriceUdpate.parsed[0].price;
const hermesPriceOracle = new OraclePrice({
price: new BN(hermesPrice.price),
exponent: new BN(hermesPrice.expo),
confidence: new BN(hermesPrice.conf),
timestamp: new BN(hermesPrice.publish_time),
});
const hermesEmaOracle = new OraclePrice({
price: new BN(hermesEma.price),
exponent: new BN(hermesEma.expo),
confidence: new BN(hermesEma.conf),
timestamp: new BN(hermesEma.publish_time),
});
const token = ALL_TOKENS.find((t) => t.pythPriceId === priceFeedId);
if (!token) {
throw new Error(`Token not found for price feed ID: ${priceFeedId}`);
}
const status = !token.isVirtual ? PriceStatus.Trading : PriceStatus.Unknown;
const pythPriceEntry: PythPriceEntry = {
price: hermesPriceOracle,
emaPrice: hermesEmaOracle,
isStale: false,
status: status,
};
return pythPriceEntry;
} catch (error) {
console.error(`Error in fetchOraclePrice for ${symbol}:`, error);
throw error;
}
};
export interface MarketInfo {
[key: string]: {
tokenPair: string;
token: string;
side: string;
pool: string;
};
}
const marketSdkInfo: MarketInfo = {};
// Loop through POOL_CONFIGS to process each market
POOL_CONFIGS.forEach((poolConfig) => {
poolConfig.markets.forEach((market) => {
const targetToken = ALL_TOKENS.find(
(token) => token.mintKey.toString() === market.targetMint.toString(),
);
// Find collateral token by matching mintKey
const collateralToken = ALL_TOKENS.find(
(token) => token.mintKey.toString() === market.collateralMint.toString(),
);
if (targetToken?.symbol && collateralToken?.symbol) {
marketSdkInfo[market.marketAccount.toString()] = {
tokenPair: `${targetToken.symbol}/${collateralToken.symbol}`,
token: targetToken.symbol,
side: Object.keys(market.side)[0],
pool: poolConfig.poolName,
};
}
});
});
export { marketSdkInfo };
export interface MarketTokenSides {
[token: string]: {
long?: { marketID: string };
short?: { marketID: string };
};
}
const marketTokenMap: MarketTokenSides = {};
// Convert marketSdkInfo into marketTokenMap
Object.entries(marketSdkInfo).forEach(([marketID, info]) => {
if (!marketTokenMap[info.token]) {
marketTokenMap[info.token] = {};
}
marketTokenMap[info.token][info.side.toLowerCase() as "long" | "short"] = {
marketID,
};
});
export { marketTokenMap };
interface TradingAccountResult {
nftReferralAccountPK: PublicKey | null;
nftTradingAccountPk: PublicKey | null;
nftOwnerRebateTokenAccountPk: PublicKey | null;
}
export async function getNftTradingAccountInfo(
userPublicKey: PublicKey,
perpClient: PerpetualsClient,
poolConfig: PoolConfig,
collateralCustodySymbol: string,
): Promise<TradingAccountResult> {
const getNFTReferralAccountPK = (publicKey: PublicKey) => {
return PublicKey.findProgramAddressSync(
[Buffer.from("referral"), publicKey.toBuffer()],
PROGRAM_ID,
)[0];
};
const nftReferralAccountPK = getNFTReferralAccountPK(userPublicKey);
const nftReferralAccountInfo =
await perpClient.provider.connection.getAccountInfo(nftReferralAccountPK);
let nftTradingAccountPk: PublicKey | null = null;
let nftOwnerRebateTokenAccountPk: PublicKey | null = null;
if (nftReferralAccountInfo) {
const nftReferralAccountData = perpClient.program.coder.accounts.decode(
"referral",
nftReferralAccountInfo.data,
) as Referral;
nftTradingAccountPk = nftReferralAccountData.refererTradingAccount;
if (nftTradingAccountPk) {
const nftTradingAccountInfo =
await perpClient.provider.connection.getAccountInfo(
nftTradingAccountPk,
);
if (nftTradingAccountInfo) {
const nftTradingAccount = perpClient.program.coder.accounts.decode(
"trading",
nftTradingAccountInfo.data,
) as { owner: PublicKey };
nftOwnerRebateTokenAccountPk = getAssociatedTokenAddressSync(
poolConfig.getTokenFromSymbol(collateralCustodySymbol).mintKey,
nftTradingAccount.owner,
);
// Check if the account exists
const accountExists =
await perpClient.provider.connection.getAccountInfo(
nftOwnerRebateTokenAccountPk,
);
if (!accountExists) {
console.error(
"NFT owner rebate token account does not exist and may need to be created",
);
}
}
}
}
return {
nftReferralAccountPK,
nftTradingAccountPk,
nftOwnerRebateTokenAccountPk,
};
}
/**
* Creates a new PerpetualsClient instance with the given connection and wallet
* @param connection Solana connection
* @param wallet Solana wallet
* @returns PerpetualsClient instance
*/
export function createPerpClient(
connection: Connection,
wallet: Keypair,
): PerpetualsClient {
const provider = new AnchorProvider(connection, new Wallet(wallet), {
commitment: "confirmed",
preflightCommitment: "confirmed",
skipPreflight: true,
});
return new PerpetualsClient(
provider,
POOL_CONFIGS[0].programId,
POOL_CONFIGS[0].perpComposibilityProgramId,
POOL_CONFIGS[0].fbNftRewardProgramId,
POOL_CONFIGS[0].rewardDistributionProgram.programId,
{},
);
}
export function get_flash_privilege(agent: SolanaAgentKit): Privilege {
const FLASH_PRIVILEGE = agent.config.FLASH_PRIVILEGE || "None";
switch (FLASH_PRIVILEGE.toLowerCase()) {
case "referral":
return Privilege.Referral;
case "nft":
return Privilege.NFT;
default:
return Privilege.None;
}
}