mirror of
https://github.com/d0zingcat/solana-agent-kit.git
synced 2026-05-14 15:10:20 +00:00
Add batch order from Manifest (#114)
# Pull Request Description ## Related Issue ## Changes Made This PR adds the following changes: <!-- List the key changes made in this PR --> - Adds batch order functionality from Manifest for easier market making ## Implementation Details <!-- Provide technical details about the implementation --> - Allow pattern based prompts for easier quote production ## Transaction executed by agent <!-- If applicable, provide example usage, transactions, or screenshots --> Example transaction: https://solscan.io/tx/64P3YHsC4Zh9GKmT6EubHfy8pEc2vYNPYF8NApb8P4LD57to1WisQfQZ7vFhwup3UgJqtGCmiRy7TW6VCrkmNTHP ## Prompt Used <!-- If relevant, include the prompt or configuration used -->  ## Additional Notes <!-- Any additional information that reviewers should know --> ## Checklist - [x] I have tested these changes locally - [x] I have updated the documentation - [x] I have added a transaction link - [x] I have added the prompt used to test it
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
request_faucet_funds,
|
||||
trade,
|
||||
limitOrder,
|
||||
batchOrder,
|
||||
cancelAllOrders,
|
||||
withdrawAll,
|
||||
transfer,
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
create_TipLink,
|
||||
listNFTForSale,
|
||||
cancelListing,
|
||||
OrderParams,
|
||||
} from "../tools";
|
||||
|
||||
import {
|
||||
@@ -184,6 +186,13 @@ export class SolanaAgentKit {
|
||||
return limitOrder(this, marketId, quantity, side, price);
|
||||
}
|
||||
|
||||
async batchOrder(
|
||||
marketId: PublicKey,
|
||||
orders: OrderParams[],
|
||||
): Promise<string> {
|
||||
return batchOrder(this, marketId, orders);
|
||||
}
|
||||
|
||||
async cancelAllOrders(marketId: PublicKey): Promise<string> {
|
||||
return cancelAllOrders(this, marketId);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../index";
|
||||
import { create_image } from "../tools/create_image";
|
||||
import { BN } from "@coral-xyz/anchor";
|
||||
import { FEE_TIERS } from "../tools";
|
||||
import { FEE_TIERS, generateOrdersfromPattern, OrderParams } from "../tools";
|
||||
|
||||
export class SolanaBalanceTool extends Tool {
|
||||
name = "solana_balance";
|
||||
@@ -310,6 +310,8 @@ export class SolanaLimitOrderTool extends Tool {
|
||||
name = "solana_limit_order";
|
||||
description = `This tool can be used to place limit orders using Manifest.
|
||||
|
||||
Do not allow users to place multiple orders with this instruction, use solana_batch_order instead.
|
||||
|
||||
Inputs ( input is a JSON string ):
|
||||
marketId: PublicKey, eg "ENhU8LsaR7vDD2G1CsWcsuSGNrih9Cv5WZEk7q9kPapQ" for SOL/USDC (required)
|
||||
quantity: number, eg 1 or 0.01 (required)
|
||||
@@ -350,6 +352,98 @@ export class SolanaLimitOrderTool extends Tool {
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaBatchOrderTool extends Tool {
|
||||
name = "solana_batch_order";
|
||||
description = `Places multiple limit orders in one transaction using Manifest. Submit orders either as a list or pattern:
|
||||
|
||||
1. List format:
|
||||
{
|
||||
"marketId": "ENhU8LsaR7vDD2G1CsWcsuSGNrih9Cv5WZEk7q9kPapQ",
|
||||
"orders": [
|
||||
{ "quantity": 1, "side": "Buy", "price": 200 },
|
||||
{ "quantity": 0.5, "side": "Sell", "price": 205 }
|
||||
]
|
||||
}
|
||||
|
||||
2. Pattern format:
|
||||
{
|
||||
"marketId": "ENhU8LsaR7vDD2G1CsWcsuSGNrih9Cv5WZEk7q9kPapQ",
|
||||
"pattern": {
|
||||
"side": "Buy",
|
||||
"totalQuantity": 100,
|
||||
"priceRange": { "max": 1.0 },
|
||||
"spacing": { "type": "percentage", "value": 1 },
|
||||
"numberOfOrders": 5
|
||||
}
|
||||
}
|
||||
|
||||
Examples:
|
||||
- "Place 5 buy orders totaling 100 tokens, 1% apart below $1"
|
||||
- "Create 3 sell orders of 10 tokens each between $50-$55"
|
||||
- "Place buy orders worth 50 tokens, $0.10 spacing from $0.80"
|
||||
|
||||
Important: All orders must be in one transaction. Combine buy and sell orders into a single pattern or list. Never break the orders down to individual buy or sell orders.`;
|
||||
|
||||
constructor(private solanaKit: SolanaAgentKit) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected async _call(input: string): Promise<string> {
|
||||
try {
|
||||
const parsedInput = JSON.parse(input);
|
||||
let ordersToPlace: OrderParams[] = [];
|
||||
|
||||
if (!parsedInput.marketId) {
|
||||
throw new Error("Market ID is required");
|
||||
}
|
||||
|
||||
if (parsedInput.pattern) {
|
||||
ordersToPlace = generateOrdersfromPattern(parsedInput.pattern);
|
||||
} else if (Array.isArray(parsedInput.orders)) {
|
||||
ordersToPlace = parsedInput.orders;
|
||||
} else {
|
||||
throw new Error("Either pattern or orders array is required");
|
||||
}
|
||||
|
||||
if (ordersToPlace.length === 0) {
|
||||
throw new Error("No orders generated or provided");
|
||||
}
|
||||
|
||||
ordersToPlace.forEach((order: OrderParams, index: number) => {
|
||||
if (!order.quantity || !order.side || !order.price) {
|
||||
throw new Error(
|
||||
`Invalid order at index ${index}: quantity, side, and price are required`,
|
||||
);
|
||||
}
|
||||
if (order.side !== "Buy" && order.side !== "Sell") {
|
||||
throw new Error(
|
||||
`Invalid side at index ${index}: must be "Buy" or "Sell"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const tx = await this.solanaKit.batchOrder(
|
||||
new PublicKey(parsedInput.marketId),
|
||||
parsedInput.orders,
|
||||
);
|
||||
|
||||
return JSON.stringify({
|
||||
status: "success",
|
||||
message: "Batch order executed successfully",
|
||||
transaction: tx,
|
||||
marketId: parsedInput.marketId,
|
||||
orders: parsedInput.orders,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return JSON.stringify({
|
||||
status: "error",
|
||||
message: error.message,
|
||||
code: error.code || "UNKNOWN_ERROR",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SolanaCancelAllOrdersTool extends Tool {
|
||||
name = "solana_cancel_all_orders";
|
||||
description = `This tool can be used to cancel all orders from a Manifest market.
|
||||
@@ -1853,6 +1947,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
|
||||
new SolanaOpenbookCreateMarket(solanaKit),
|
||||
new SolanaManifestCreateMarket(solanaKit),
|
||||
new SolanaLimitOrderTool(solanaKit),
|
||||
new SolanaBatchOrderTool(solanaKit),
|
||||
new SolanaCancelAllOrdersTool(solanaKit),
|
||||
new SolanaWithdrawAllTool(solanaKit),
|
||||
new SolanaClosePosition(solanaKit),
|
||||
|
||||
172
src/tools/batch_order.ts
Normal file
172
src/tools/batch_order.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import {
|
||||
PublicKey,
|
||||
Transaction,
|
||||
sendAndConfirmTransaction,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { SolanaAgentKit } from "../index";
|
||||
import {
|
||||
ManifestClient,
|
||||
WrapperPlaceOrderParamsExternal,
|
||||
} from "@cks-systems/manifest-sdk";
|
||||
import { OrderType } from "@cks-systems/manifest-sdk/client/ts/src/wrapper/types/OrderType";
|
||||
|
||||
export interface OrderParams {
|
||||
quantity: number;
|
||||
side: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface BatchOrderPattern {
|
||||
side: string;
|
||||
totalQuantity?: number;
|
||||
priceRange?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
};
|
||||
spacing?: {
|
||||
type: "percentage" | "fixed";
|
||||
value: number;
|
||||
};
|
||||
numberOfOrders?: number;
|
||||
individualQuantity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of orders based on the specified pattern
|
||||
*/
|
||||
export function generateOrdersfromPattern(
|
||||
pattern: BatchOrderPattern,
|
||||
): OrderParams[] {
|
||||
const orders: OrderParams[] = [];
|
||||
|
||||
// Random number of orders if not specified, max of 8
|
||||
const numOrders = pattern.numberOfOrders || Math.ceil(Math.random() * 8);
|
||||
|
||||
// Calculate price points
|
||||
const prices: number[] = [];
|
||||
if (pattern.priceRange) {
|
||||
const { min, max } = pattern.priceRange;
|
||||
if (min && max) {
|
||||
// Generate evenly spaced prices
|
||||
for (let i = 0; i < numOrders; i++) {
|
||||
if (pattern.spacing?.type === "percentage") {
|
||||
const factor = 1 + pattern.spacing.value / 100;
|
||||
prices.push(min * Math.pow(factor, i));
|
||||
} else {
|
||||
const step = (max - min) / (numOrders - 1);
|
||||
prices.push(min + step * i);
|
||||
}
|
||||
}
|
||||
} else if (min) {
|
||||
// Generate prices starting from min with specified spacing
|
||||
for (let i = 0; i < numOrders; i++) {
|
||||
if (pattern.spacing?.type === "percentage") {
|
||||
const factor = 1 + pattern.spacing.value / 100;
|
||||
prices.push(min * Math.pow(factor, i));
|
||||
} else {
|
||||
prices.push(min + (pattern.spacing?.value || 0.01) * i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate quantities
|
||||
let quantities: number[] = [];
|
||||
if (pattern.totalQuantity) {
|
||||
const individualQty = pattern.totalQuantity / numOrders;
|
||||
quantities = Array(numOrders).fill(individualQty);
|
||||
} else if (pattern.individualQuantity) {
|
||||
quantities = Array(numOrders).fill(pattern.individualQuantity);
|
||||
}
|
||||
|
||||
// Generate orders
|
||||
for (let i = 0; i < numOrders; i++) {
|
||||
orders.push({
|
||||
side: pattern.side,
|
||||
price: prices[i],
|
||||
quantity: quantities[i],
|
||||
});
|
||||
}
|
||||
|
||||
return orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that sell orders are not priced below buy orders
|
||||
* @param orders Array of order parameters to validate
|
||||
* @throws Error if orders are crossed
|
||||
*/
|
||||
function validateNoCrossedOrders(orders: OrderParams[]): void {
|
||||
// Find lowest sell and highest buy prices
|
||||
let lowestSell = Number.MAX_SAFE_INTEGER;
|
||||
let highestBuy = 0;
|
||||
|
||||
orders.forEach((order) => {
|
||||
if (order.side === "Sell" && order.price < lowestSell) {
|
||||
lowestSell = order.price;
|
||||
}
|
||||
if (order.side === "Buy" && order.price > highestBuy) {
|
||||
highestBuy = order.price;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if orders cross
|
||||
if (lowestSell <= highestBuy) {
|
||||
throw new Error(
|
||||
`Invalid order prices: Sell order at ${lowestSell} is lower than or equal to Buy order at ${highestBuy}. Orders cannot cross.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Place batch 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 batchOrder(
|
||||
agent: SolanaAgentKit,
|
||||
marketId: PublicKey,
|
||||
orders: OrderParams[],
|
||||
): Promise<string> {
|
||||
try {
|
||||
validateNoCrossedOrders(orders);
|
||||
|
||||
const mfxClient = await ManifestClient.getClientForMarket(
|
||||
agent.connection,
|
||||
marketId,
|
||||
agent.wallet,
|
||||
);
|
||||
|
||||
const placeParams: WrapperPlaceOrderParamsExternal[] = orders.map(
|
||||
(order) => ({
|
||||
numBaseTokens: order.quantity,
|
||||
tokenPrice: order.price,
|
||||
isBid: order.side === "Buy",
|
||||
lastValidSlot: 0,
|
||||
orderType: OrderType.Limit,
|
||||
clientOrderId: Number(Math.random() * 10000),
|
||||
}),
|
||||
);
|
||||
|
||||
const batchOrderIx: TransactionInstruction = await mfxClient.batchUpdateIx(
|
||||
placeParams,
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
const signature = await sendAndConfirmTransaction(
|
||||
agent.connection,
|
||||
new Transaction().add(batchOrderIx),
|
||||
[agent.wallet],
|
||||
);
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
throw new Error(`Batch Order failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export * from "./mint_nft";
|
||||
export * from "./transfer";
|
||||
export * from "./trade";
|
||||
export * from "./limit_order";
|
||||
export * from "./batch_order";
|
||||
export * from "./cancel_all_orders";
|
||||
export * from "./withdraw_all";
|
||||
export * from "./register_domain";
|
||||
|
||||
Reference in New Issue
Block a user