Skip to content

Commit bfa7872

Browse files
authored
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 --> ![image](https://github.com/user-attachments/assets/d62d8908-5de5-4784-adf3-8a3a2afa3ae7) ## 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
2 parents 5664803 + e8f8e45 commit bfa7872

File tree

4 files changed

+278
-1
lines changed

4 files changed

+278
-1
lines changed

src/agent/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
request_faucet_funds,
2424
trade,
2525
limitOrder,
26+
batchOrder,
2627
cancelAllOrders,
2728
withdrawAll,
2829
transfer,
@@ -50,6 +51,7 @@ import {
5051
create_TipLink,
5152
listNFTForSale,
5253
cancelListing,
54+
OrderParams,
5355
} from "../tools";
5456

5557
import {
@@ -184,6 +186,13 @@ export class SolanaAgentKit {
184186
return limitOrder(this, marketId, quantity, side, price);
185187
}
186188

189+
async batchOrder(
190+
marketId: PublicKey,
191+
orders: OrderParams[],
192+
): Promise<string> {
193+
return batchOrder(this, marketId, orders);
194+
}
195+
187196
async cancelAllOrders(marketId: PublicKey): Promise<string> {
188197
return cancelAllOrders(this, marketId);
189198
}

src/langchain/index.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "../index";
99
import { create_image } from "../tools/create_image";
1010
import { BN } from "@coral-xyz/anchor";
11-
import { FEE_TIERS } from "../tools";
11+
import { FEE_TIERS, generateOrdersfromPattern, OrderParams } from "../tools";
1212

1313
export class SolanaBalanceTool extends Tool {
1414
name = "solana_balance";
@@ -310,6 +310,8 @@ export class SolanaLimitOrderTool extends Tool {
310310
name = "solana_limit_order";
311311
description = `This tool can be used to place limit orders using Manifest.
312312
313+
Do not allow users to place multiple orders with this instruction, use solana_batch_order instead.
314+
313315
Inputs ( input is a JSON string ):
314316
marketId: PublicKey, eg "ENhU8LsaR7vDD2G1CsWcsuSGNrih9Cv5WZEk7q9kPapQ" for SOL/USDC (required)
315317
quantity: number, eg 1 or 0.01 (required)
@@ -350,6 +352,98 @@ export class SolanaLimitOrderTool extends Tool {
350352
}
351353
}
352354

355+
export class SolanaBatchOrderTool extends Tool {
356+
name = "solana_batch_order";
357+
description = `Places multiple limit orders in one transaction using Manifest. Submit orders either as a list or pattern:
358+
359+
1. List format:
360+
{
361+
"marketId": "ENhU8LsaR7vDD2G1CsWcsuSGNrih9Cv5WZEk7q9kPapQ",
362+
"orders": [
363+
{ "quantity": 1, "side": "Buy", "price": 200 },
364+
{ "quantity": 0.5, "side": "Sell", "price": 205 }
365+
]
366+
}
367+
368+
2. Pattern format:
369+
{
370+
"marketId": "ENhU8LsaR7vDD2G1CsWcsuSGNrih9Cv5WZEk7q9kPapQ",
371+
"pattern": {
372+
"side": "Buy",
373+
"totalQuantity": 100,
374+
"priceRange": { "max": 1.0 },
375+
"spacing": { "type": "percentage", "value": 1 },
376+
"numberOfOrders": 5
377+
}
378+
}
379+
380+
Examples:
381+
- "Place 5 buy orders totaling 100 tokens, 1% apart below $1"
382+
- "Create 3 sell orders of 10 tokens each between $50-$55"
383+
- "Place buy orders worth 50 tokens, $0.10 spacing from $0.80"
384+
385+
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.`;
386+
387+
constructor(private solanaKit: SolanaAgentKit) {
388+
super();
389+
}
390+
391+
protected async _call(input: string): Promise<string> {
392+
try {
393+
const parsedInput = JSON.parse(input);
394+
let ordersToPlace: OrderParams[] = [];
395+
396+
if (!parsedInput.marketId) {
397+
throw new Error("Market ID is required");
398+
}
399+
400+
if (parsedInput.pattern) {
401+
ordersToPlace = generateOrdersfromPattern(parsedInput.pattern);
402+
} else if (Array.isArray(parsedInput.orders)) {
403+
ordersToPlace = parsedInput.orders;
404+
} else {
405+
throw new Error("Either pattern or orders array is required");
406+
}
407+
408+
if (ordersToPlace.length === 0) {
409+
throw new Error("No orders generated or provided");
410+
}
411+
412+
ordersToPlace.forEach((order: OrderParams, index: number) => {
413+
if (!order.quantity || !order.side || !order.price) {
414+
throw new Error(
415+
`Invalid order at index ${index}: quantity, side, and price are required`,
416+
);
417+
}
418+
if (order.side !== "Buy" && order.side !== "Sell") {
419+
throw new Error(
420+
`Invalid side at index ${index}: must be "Buy" or "Sell"`,
421+
);
422+
}
423+
});
424+
425+
const tx = await this.solanaKit.batchOrder(
426+
new PublicKey(parsedInput.marketId),
427+
parsedInput.orders,
428+
);
429+
430+
return JSON.stringify({
431+
status: "success",
432+
message: "Batch order executed successfully",
433+
transaction: tx,
434+
marketId: parsedInput.marketId,
435+
orders: parsedInput.orders,
436+
});
437+
} catch (error: any) {
438+
return JSON.stringify({
439+
status: "error",
440+
message: error.message,
441+
code: error.code || "UNKNOWN_ERROR",
442+
});
443+
}
444+
}
445+
}
446+
353447
export class SolanaCancelAllOrdersTool extends Tool {
354448
name = "solana_cancel_all_orders";
355449
description = `This tool can be used to cancel all orders from a Manifest market.
@@ -1853,6 +1947,7 @@ export function createSolanaTools(solanaKit: SolanaAgentKit) {
18531947
new SolanaOpenbookCreateMarket(solanaKit),
18541948
new SolanaManifestCreateMarket(solanaKit),
18551949
new SolanaLimitOrderTool(solanaKit),
1950+
new SolanaBatchOrderTool(solanaKit),
18561951
new SolanaCancelAllOrdersTool(solanaKit),
18571952
new SolanaWithdrawAllTool(solanaKit),
18581953
new SolanaClosePosition(solanaKit),

src/tools/batch_order.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
PublicKey,
3+
Transaction,
4+
sendAndConfirmTransaction,
5+
TransactionInstruction,
6+
} from "@solana/web3.js";
7+
import { SolanaAgentKit } from "../index";
8+
import {
9+
ManifestClient,
10+
WrapperPlaceOrderParamsExternal,
11+
} from "@cks-systems/manifest-sdk";
12+
import { OrderType } from "@cks-systems/manifest-sdk/client/ts/src/wrapper/types/OrderType";
13+
14+
export interface OrderParams {
15+
quantity: number;
16+
side: string;
17+
price: number;
18+
}
19+
20+
interface BatchOrderPattern {
21+
side: string;
22+
totalQuantity?: number;
23+
priceRange?: {
24+
min?: number;
25+
max?: number;
26+
};
27+
spacing?: {
28+
type: "percentage" | "fixed";
29+
value: number;
30+
};
31+
numberOfOrders?: number;
32+
individualQuantity?: number;
33+
}
34+
35+
/**
36+
* Generates an array of orders based on the specified pattern
37+
*/
38+
export function generateOrdersfromPattern(
39+
pattern: BatchOrderPattern,
40+
): OrderParams[] {
41+
const orders: OrderParams[] = [];
42+
43+
// Random number of orders if not specified, max of 8
44+
const numOrders = pattern.numberOfOrders || Math.ceil(Math.random() * 8);
45+
46+
// Calculate price points
47+
const prices: number[] = [];
48+
if (pattern.priceRange) {
49+
const { min, max } = pattern.priceRange;
50+
if (min && max) {
51+
// Generate evenly spaced prices
52+
for (let i = 0; i < numOrders; i++) {
53+
if (pattern.spacing?.type === "percentage") {
54+
const factor = 1 + pattern.spacing.value / 100;
55+
prices.push(min * Math.pow(factor, i));
56+
} else {
57+
const step = (max - min) / (numOrders - 1);
58+
prices.push(min + step * i);
59+
}
60+
}
61+
} else if (min) {
62+
// Generate prices starting from min with specified spacing
63+
for (let i = 0; i < numOrders; i++) {
64+
if (pattern.spacing?.type === "percentage") {
65+
const factor = 1 + pattern.spacing.value / 100;
66+
prices.push(min * Math.pow(factor, i));
67+
} else {
68+
prices.push(min + (pattern.spacing?.value || 0.01) * i);
69+
}
70+
}
71+
}
72+
}
73+
74+
// Calculate quantities
75+
let quantities: number[] = [];
76+
if (pattern.totalQuantity) {
77+
const individualQty = pattern.totalQuantity / numOrders;
78+
quantities = Array(numOrders).fill(individualQty);
79+
} else if (pattern.individualQuantity) {
80+
quantities = Array(numOrders).fill(pattern.individualQuantity);
81+
}
82+
83+
// Generate orders
84+
for (let i = 0; i < numOrders; i++) {
85+
orders.push({
86+
side: pattern.side,
87+
price: prices[i],
88+
quantity: quantities[i],
89+
});
90+
}
91+
92+
return orders;
93+
}
94+
95+
/**
96+
* Validates that sell orders are not priced below buy orders
97+
* @param orders Array of order parameters to validate
98+
* @throws Error if orders are crossed
99+
*/
100+
function validateNoCrossedOrders(orders: OrderParams[]): void {
101+
// Find lowest sell and highest buy prices
102+
let lowestSell = Number.MAX_SAFE_INTEGER;
103+
let highestBuy = 0;
104+
105+
orders.forEach((order) => {
106+
if (order.side === "Sell" && order.price < lowestSell) {
107+
lowestSell = order.price;
108+
}
109+
if (order.side === "Buy" && order.price > highestBuy) {
110+
highestBuy = order.price;
111+
}
112+
});
113+
114+
// Check if orders cross
115+
if (lowestSell <= highestBuy) {
116+
throw new Error(
117+
`Invalid order prices: Sell order at ${lowestSell} is lower than or equal to Buy order at ${highestBuy}. Orders cannot cross.`,
118+
);
119+
}
120+
}
121+
122+
/**
123+
* Place batch orders using Manifest
124+
* @param agent SolanaAgentKit instance
125+
* @param marketId Public key for the manifest market
126+
* @param quantity Amount to trade in tokens
127+
* @param side Buy or Sell
128+
* @param price Price in tokens ie. SOL/USDC
129+
* @returns Transaction signature
130+
*/
131+
export async function batchOrder(
132+
agent: SolanaAgentKit,
133+
marketId: PublicKey,
134+
orders: OrderParams[],
135+
): Promise<string> {
136+
try {
137+
validateNoCrossedOrders(orders);
138+
139+
const mfxClient = await ManifestClient.getClientForMarket(
140+
agent.connection,
141+
marketId,
142+
agent.wallet,
143+
);
144+
145+
const placeParams: WrapperPlaceOrderParamsExternal[] = orders.map(
146+
(order) => ({
147+
numBaseTokens: order.quantity,
148+
tokenPrice: order.price,
149+
isBid: order.side === "Buy",
150+
lastValidSlot: 0,
151+
orderType: OrderType.Limit,
152+
clientOrderId: Number(Math.random() * 10000),
153+
}),
154+
);
155+
156+
const batchOrderIx: TransactionInstruction = await mfxClient.batchUpdateIx(
157+
placeParams,
158+
[],
159+
true,
160+
);
161+
162+
const signature = await sendAndConfirmTransaction(
163+
agent.connection,
164+
new Transaction().add(batchOrderIx),
165+
[agent.wallet],
166+
);
167+
168+
return signature;
169+
} catch (error: any) {
170+
throw new Error(`Batch Order failed: ${error.message}`);
171+
}
172+
}

src/tools/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./mint_nft";
77
export * from "./transfer";
88
export * from "./trade";
99
export * from "./limit_order";
10+
export * from "./batch_order";
1011
export * from "./cancel_all_orders";
1112
export * from "./withdraw_all";
1213
export * from "./register_domain";

0 commit comments

Comments
 (0)