Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
thearyanag authored Jan 3, 2025
2 parents 376eaad + bfa7872 commit b3a02fe
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 2 deletions.
2 changes: 1 addition & 1 deletion examples/persistent-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
9 changes: 9 additions & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
request_faucet_funds,
trade,
limitOrder,
batchOrder,
cancelAllOrders,
withdrawAll,
transfer,
Expand Down Expand Up @@ -52,6 +53,7 @@ import {
cancelListing,
fetchTokenReportSummary,
fetchTokenDetailedReport,
OrderParams,
} from "../tools";

import {
Expand Down Expand Up @@ -192,6 +194,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);
}
Expand Down
97 changes: 96 additions & 1 deletion src/langchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1914,6 +2008,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),
Expand Down
172 changes: 172 additions & 0 deletions src/tools/batch_order.ts
Original file line number Diff line number Diff line change
@@ -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}`);
}
}
1 change: 1 addition & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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";
Expand Down

0 comments on commit b3a02fe

Please sign in to comment.