Skip to content

[WIP](TON Plugin) Add support Liquidity Pool Management #3253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from
6 changes: 6 additions & 0 deletions packages/plugin-ton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@
],
"dependencies": {
"@elizaos/core": "workspace:*",
"@ton/core": "^0.60.0",
"@ton/crypto": "3.3.0",
"@ton/ton": "15.1.0",
"@torch-finance/core": "^1.3.2",
"@torch-finance/dex-contract-wrapper": "^0.2.9",
"@torch-finance/sdk": "^1.1.0",
"@torch-finance/simulator": "^0.3.5",
"@torch-finance/wallet-utils": "^0.2.0",
"bignumber.js": "9.1.2",
"node-cache": "5.1.2",
"tsup": "8.3.5"
Expand Down
219 changes: 219 additions & 0 deletions packages/plugin-ton/src/actions/createLiquidityPool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import {
composeContext,
Content,
elizaLogger,
generateObject,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
} from "@elizaos/core";
import {
initWalletProvider,
nativeWalletProvider,
WalletProvider,
} from "../providers/wallet";
import { base64ToHex, sleep } from "../utils/util";
import { z } from "zod";
import { SUPPORTED_DEXES } from "./pools";

interface ActionOptions {
[key: string]: unknown;
}

// create new liquidity pools
// handle deposits/withdrawals
// and manage fees

// TODO
const createPoolTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.

Example response:
\`\`\`json
{
"dex": "TORCH_FINANCE",
"tokenA": "TON",
"tokenB": "stTON",
"initialLiquidity": "1"
}
\`\`\`

{{recentMessages}}

Given the recent messages, extract the following information about the requested token transfer:
- The name of the DEX the user wants to work with, from the following list: ["TORCH_FINANCE", "RAINBOW_SWAP", "STON_FI", "DEDUST"]
- The name of the first token of the token pair the user wants to create
- The name of the second token of the token pair the user wants to create
- Amount of intial liquidity they want to provide

Respond with a JSON markdown block containing only the extracted values.`;

interface PoolCreatetionContent extends Content {
dex: (typeof SUPPORTED_DEXES)[number];
tokenA: string;
tokenB: string;
initialLiquidity: string;
}

export class PoolCreationAction {
private walletProvider: WalletProvider;

constructor(walletProvider: WalletProvider) {
this.walletProvider = walletProvider;
}

async createPool(params: PoolCreatetionContent): Promise<string> {
console.log(
`(${params.dex})Creating: ${params.tokenA}-${params.tokenB} pool w/ (${params.initialLiquidity}) initial liquidity`
);

const walletClient = this.walletProvider.getWalletClient();
const contract = walletClient.open(this.walletProvider.wallet);

try {
const seqno: number = await contract.getSeqno();
await sleep(1500);

// TODO create pool w/ third-party sdk
await sleep(1500);

console.log("Transaction sent, still waiting for confirmation...");
await sleep(1500);
//this.waitForTransaction(seqno, contract);
const state = await walletClient.getContractState(
this.walletProvider.wallet.address
);
const { lt: _, hash: lastHash } = state.lastTransaction;
return base64ToHex(lastHash);
} catch (error) {
throw new Error(`Transfer failed: ${error.message}`);
}
}
}

const buildPoolCreationDetails = async (
runtime: IAgentRuntime,
message: Memory,
state: State
): Promise<PoolCreatetionContent> => {
const walletInfo = await nativeWalletProvider.get(runtime, message, state);
state.walletInfo = walletInfo;

// Initialize or update state
let currentState = state;
if (!currentState) {
currentState = (await runtime.composeState(message)) as State;
} else {
currentState = await runtime.updateRecentMessageState(currentState);
}

// Define the schema for the expected output
const poolCreationSchema = z.object({
tokenA: z.string(),
tokenB: z.string(),
initialLiquidity: z.union([z.string(), z.number()]),
});

// Compose transfer context
const createPoolContext = composeContext({
state,
template: createPoolTemplate,
});

// Generate transfer content with the schema
const content = await generateObject({
runtime,
context: createPoolContext,
schema: poolCreationSchema,
modelClass: ModelClass.SMALL,
});

let transferContent: PoolCreatetionContent =
content.object as PoolCreatetionContent;

if (transferContent === undefined) {
transferContent = content as unknown as PoolCreatetionContent;
}

return transferContent;
};

function isPoolCreationContent(
content: Content
): content is PoolCreatetionContent {
console.log("Content for pool creation", content);
return (
typeof content.tokenA === "string" &&
typeof content.tokenB === "string" &&
typeof content.initialLiuqidity === "number"
);
}

export default {
name: "CREATE_LIQUIDITY_POOL",
similes: ["CREATE_POOL"],
description:
"Create a liquidity pool, given a token pair and initial liquidity",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: ActionOptions,
callback?: HandlerCallback
) => {
elizaLogger.log("Starting SEND_TOKEN handler...");

const poolCreationDetails = await buildPoolCreationDetails(
runtime,
message,
state
);

// Validate transfer content
if (!isPoolCreationContent(poolCreationDetails)) {
console.error("Invalid content for TRANSFER_TOKEN action.");
if (callback) {
callback({
text: "Unable to process transfer request. Invalid content provided.",
content: { error: "Invalid transfer content" },
});
}
return false;
}

try {
const walletProvider = await initWalletProvider(runtime);
const action = new PoolCreationAction(walletProvider);
const hash = await action.createPool(poolCreationDetails);

// if (callback) {
// callback({
// // TODO wait for transaction to complete
// text: `Successfully created pool ${poolCreationDetails.address}, Transaction: ${hash}`,
// content: {
// success: true,
// hash: hash,
// amount: poolCreationDetails.amount,
// recipient: poolCreationDetails.recipient,
// },
// });
// }

return true;
} catch (error) {
console.error("Error during token transfer:", error);
if (callback) {
callback({
text: `Error transferring tokens: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
template: createPoolTemplate,
// eslint-disable-next-line
validate: async (_runtime: IAgentRuntime) => true,
examples: [],
};
26 changes: 26 additions & 0 deletions packages/plugin-ton/src/actions/pools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export * from './torchFinance';

export const SUPPORTED_DEXES = ["TORCH_FINANCE", "RAINBOW_SWAP", "STON_FI", "DEDUST"];

export type Token = {
address: string;
name: string;
};

export type DepositAsset = {
token: Token;
amount: number;
};

export interface LiquidityPool {
name: string;
createPool: (tokenA: Token, tokenB: Token, initialLiquidity: number) => {};
// LP tokens should be issued
deposit: (assets: DepositAsset[]) => {};
// LP tokens should be burned
withdraw: () => {};
claimFee: () => {};
}

const isPoolSupported = (poolName: string) =>
SUPPORTED_DEXES.includes(poolName);
128 changes: 128 additions & 0 deletions packages/plugin-ton/src/actions/pools/torchFinance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { DepositAsset, LiquidityPool, Token } from ".";
import {
TorchSDK,
generateQueryId,
DepositParams,
toUnit,
} from "@torch-finance/sdk";
import { Asset } from "@torch-finance/core";
import { OpenedContract, WalletContractV5R1 } from "@ton/ton";
import { IAgentRuntime } from "@elizaos/core";
import { CONFIG_KEYS } from "../../enviroment";
import { createWalletV5 } from "@torch-finance/wallet-utils";
import { WalletProvider } from "../../providers/wallet";
import { mnemonicToWalletKey } from "@ton/crypto";

const sdk = new TorchSDK();

const TON_ASSET = Asset.ton();
const TSTON_ASSET = Asset.jetton(
"EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav"
);
const STTON_ASSET = Asset.jetton(
"EQDNhy-nxYFgUqzfUzImBEP67JqsyMIcyk2S5_RwNNEYku0k"
);

const SUPPORTED_TOKENS = [
"EQC98_qAmNEptUtPc7W6xdHh_ZHrBUFpw5Ft_IzNU20QAJav",
"EQDNhy-nxYFgUqzfUzImBEP67JqsyMIcyk2S5_RwNNEYku0k",
"EQDPdq8xjAhytYqfGSX8KcFWIReCufsB9Wdg0pLlYSO_h76w",
];

const TriTONPoolAddress = "EQD_pool_address_example";

const PROVIDER_CONFIG = {
MAINNET_RPC: "https://toncenter.com/api/v2/jsonRPC",
RPC_API_KEY: "",
STONFI_TON_USD_POOL: "EQCGScrZe1xbyWqWDvdI6mzP-GAcAWFv6ZXuaJOuSqemxku4",
CHAIN_NAME_IN_DEXSCREENER: "ton",
// USD_DECIMAL=10^6
MAX_RETRIES: 3,
RETRY_DELAY: 2000,
// 10^9
TON_DECIMAL: BigInt(1000000000),
};

export class TorchFinance implements LiquidityPool {
name: string;
wallet: OpenedContract<WalletContractV5R1>;
send: any;

async initWallet(runtime: IAgentRuntime) {
const privateKey = runtime.getSetting(CONFIG_KEYS.TON_PRIVATE_KEY);
// Removed unnecessary else clause
if (!privateKey) {
throw new Error(`${CONFIG_KEYS.TON_PRIVATE_KEY} is missing`);
}

const mnemonics = privateKey.split(" ");
if (mnemonics.length < 2) {
throw new Error(
`${CONFIG_KEYS.TON_PRIVATE_KEY} mnemonic seems invalid`
);
}

const rpcUrl =
runtime.getSetting("TON_RPC_URL") || PROVIDER_CONFIG.MAINNET_RPC;
const keypair = await mnemonicToWalletKey(mnemonics, "");
const walletProvider = new WalletProvider(
keypair,
rpcUrl,
runtime.cacheManager
);
const { wallet, send } = await createWalletV5(
walletProvider.getWalletClient(),
mnemonics,
"testnet"
);
this.wallet = wallet;
this.send = send;
}

// Not supported
async createPool(tokenA: Token, tokenB: Token, initialLiquidity: number) {
return false;
}

supportedTokens() {
return SUPPORTED_TOKENS.push("TON");
}

async deposit(assets: DepositAsset[], slippageTolerance?: number, pool?: string) {
const queryId = await generateQueryId();
assets.forEach((asset) => {
if (!SUPPORTED_TOKENS.includes(asset.token.address)) {
throw new Error("Unsupported asset");
}
});

const depositParams: DepositParams = {
queryId,
// TODO Look supported pools
pool: pool ?? TriTONPoolAddress,
depositAmounts: assets.map((asset) => {
return {
asset: asset.token.name == 'TON' ? TON_ASSET : asset.token.name === 'STTON_TOKEN' ? STTON_ASSET : TSTON_ASSET,
value: toUnit(asset.amount, 9),
};
}),
slippageTolerance: slippageTolerance ?? 0.01, // 1%
};

const { result, getDepositPayload } = await sdk.simulateDeposit(
depositParams
);

const sender = this.wallet.address;
const senderArgs = await getDepositPayload(sender, {
...depositParams,
blockNumber: 27724599,
});

const msgHash = await this.send(senderArgs);
console.log(`Transaction sent with msghash: ${msgHash}`);
}

async withdraw() {}
async claimFee() {}
}
Loading