From e8f8e453e5ad3aa06852391d4981a627aa6ccf16 Mon Sep 17 00:00:00 2001 From: DonDuala Date: Wed, 1 Jan 2025 18:35:14 -0400 Subject: [PATCH 1/2] Add batch order from Manifest --- src/agent/index.ts | 9 ++ src/langchain/index.ts | 97 +++++++++++++++++++++- src/tools/batch_order.ts | 172 +++++++++++++++++++++++++++++++++++++++ src/tools/index.ts | 1 + 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 src/tools/batch_order.ts diff --git a/src/agent/index.ts b/src/agent/index.ts index 4acf694..d0b017a 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -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 { + return batchOrder(this, marketId, orders); + } + async cancelAllOrders(marketId: PublicKey): Promise { return cancelAllOrders(this, marketId); } diff --git a/src/langchain/index.ts b/src/langchain/index.ts index 4605fd3..4c50cc7 100644 --- a/src/langchain/index.ts +++ b/src/langchain/index.ts @@ -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 { + 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), diff --git a/src/tools/batch_order.ts b/src/tools/batch_order.ts new file mode 100644 index 0000000..f788ba4 --- /dev/null +++ b/src/tools/batch_order.ts @@ -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 { + 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}`); + } +} diff --git a/src/tools/index.ts b/src/tools/index.ts index b9f7542..2855459 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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"; From f1c5213742c26fa358d1d0a72d4229802f63c366 Mon Sep 17 00:00:00 2001 From: teenager-ETH Date: Thu, 2 Jan 2025 19:46:36 +0100 Subject: [PATCH 2/2] typo fix Update README.md --- examples/persistent-agent/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/persistent-agent/README.md b/examples/persistent-agent/README.md index 1ba4d37..133c179 100644 --- a/examples/persistent-agent/README.md +++ b/examples/persistent-agent/README.md @@ -13,7 +13,7 @@ This example showcases a persistent agent that retains memory across sessions us To use this feature, ensure you have the following: -1. **PostgreSQL Database URL**: Create and host ur PostgreSQL databse and enter the URL. It will be of the format "postgresql://user:password@localhost:5432/db" +1. **PostgreSQL Database URL**: Create and host ur PostgreSQL database and enter the URL. It will be of the format "postgresql://user:password@localhost:5432/db" ## Without persistence ```