Skip to content
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

feat: Add NEAR Protocol plugin #847

Merged
merged 14 commits into from
Dec 14, 2024
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,11 @@ WHATSAPP_API_VERSION=v17.0 # WhatsApp API version (default: v17.0)
# ICP
INTERNET_COMPUTER_PRIVATE_KEY=
INTERNET_COMPUTER_ADDRESS=

# NEAR
NEAR_WALLET_SECRET_KEY=
NEAR_WALLET_PUBLIC_KEY=
NEAR_ADDRESS=
SLIPPAGE=1
RPC_URL=https://rpc.testnet.near.org
NEAR_NETWORK=testnet # or mainnet
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@ai16z/plugin-icp": "workspace:*",
"@ai16z/plugin-tee": "workspace:*",
"@ai16z/plugin-coinbase": "workspace:*",
"@ai16z/plugin-near": "workspace:*",
"readline": "1.3.0",
"ws": "8.18.0",
"@ai16z/plugin-evm": "workspace:*",
Expand Down
9 changes: 7 additions & 2 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { evmPlugin } from "@ai16z/plugin-evm";
import { createNodePlugin } from "@ai16z/plugin-node";
import { solanaPlugin } from "@ai16z/plugin-solana";
import { teePlugin } from "@ai16z/plugin-tee";
import { nearPlugin } from "@ai16z/plugin-near";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
Expand Down Expand Up @@ -372,7 +373,12 @@ export function createAgent(
!getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))
? solanaPlugin
: null,
getSecret(character, "EVM_PRIVATE_KEY") ||
(getSecret(character, "NEAR_ADDRESS") ||
getSecret(character, "NEAR_WALLET_PUBLIC_KEY")) &&
getSecret(character, "NEAR_WALLET_SECRET_KEY")
? nearPlugin
: null,
getSecret(character, "EVM_PUBLIC_KEY") ||
(getSecret(character, "WALLET_PUBLIC_KEY") &&
!getSecret(character, "WALLET_PUBLIC_KEY")?.startsWith("0x"))
? evmPlugin
Expand Down Expand Up @@ -433,7 +439,6 @@ async function startAgent(character: Character, directClient) {

const cache = intializeDbCache(character, db);
const runtime = createAgent(character, db, cache, token);

await runtime.initialize();

const clients = await initializeClients(character, runtime);
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-near/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@ai16z/plugin-near",
"version": "0.0.1",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@ai16z/eliza": "workspace:*",
"@ai16z/plugin-trustdb": "workspace:*",
"@ref-finance/ref-sdk": "^1.4.6",
"tsup": "8.3.5",
"near-api-js": "5.0.1",
"bignumber.js": "9.1.2",
"node-cache": "5.1.2"
},
"devDependencies": {
"eslint": "^9.15.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-vitest": "0.5.4"
},
"scripts": {
"build": "tsup --format esm,cjs --dts",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint . --fix"
},
"peerDependencies": {
"whatwg-url": "7.1.0",
"form-data": "4.0.1"
}
}
295 changes: 295 additions & 0 deletions packages/plugin-near/src/actions/swap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import {
ActionExample,
HandlerCallback,
IAgentRuntime,
Memory,
ModelClass,
State,
type Action,
composeContext,
generateObject,
} from "@ai16z/eliza";
import { connect, keyStores, utils } from "near-api-js";
import { init_env, ftGetTokenMetadata, estimateSwap, instantSwap, fetchAllPools, FT_MINIMUM_STORAGE_BALANCE_LARGE, ONE_YOCTO_NEAR } from '@ref-finance/ref-sdk';
import { walletProvider } from "../providers/wallet";
import { KeyPairString } from "near-api-js/lib/utils";


async function checkStorageBalance(account: any, contractId: string): Promise<boolean> {
try {
const balance = await account.viewFunction({
contractId,
methodName: 'storage_balance_of',
args: { account_id: account.accountId }
});
return balance !== null && balance.total !== '0';
} catch (error) {
console.log(`Error checking storage balance: ${error}`);
return false;
}
}

async function swapToken(
runtime: IAgentRuntime,
inputTokenId: string,
outputTokenId: string,
amount: string,
slippageTolerance: number = Number(runtime.getSetting("SLIPPAGE_TOLERANCE")) || 0.01
): Promise<any> {
try {
// Get token metadata
const tokenIn = await ftGetTokenMetadata(inputTokenId);
const tokenOut = await ftGetTokenMetadata(outputTokenId);
const networkId = runtime.getSetting("NEAR_NETWORK") || "testnet";
const nodeUrl = runtime.getSetting("RPC_URL") || "https://rpc.testnet.near.org";

// Get all pools for estimation
const { ratedPools, unRatedPools, simplePools} = await fetchAllPools();
const swapTodos = await estimateSwap({
tokenIn,
tokenOut,
amountIn: amount,
simplePools,
options: {
enableSmartRouting: true,
}
});

if (!swapTodos || swapTodos.length === 0) {
throw new Error('No valid swap route found');
}

// Get account ID from runtime settings
const accountId = runtime.getSetting("NEAR_ADDRESS");
if (!accountId) {
throw new Error("NEAR_ADDRESS not configured");
}

const secretKey = runtime.getSetting("NEAR_WALLET_SECRET_KEY");
const keyStore = new keyStores.InMemoryKeyStore();
const keyPair = utils.KeyPair.fromString(secretKey as KeyPairString);
await keyStore.setKey(networkId, accountId, keyPair);

const nearConnection = await connect({
networkId,
keyStore,
nodeUrl,
});

const account = await nearConnection.account(accountId);

// Check storage balance for both tokens
const hasStorageIn = await checkStorageBalance(account, inputTokenId);
const hasStorageOut = await checkStorageBalance(account, outputTokenId);

let transactions = await instantSwap({
tokenIn,
tokenOut,
amountIn: amount,
swapTodos,
slippageTolerance,
AccountId: accountId
});

// If storage deposit is needed, add it to transactions
if (!hasStorageIn) {
transactions.unshift({
receiverId: inputTokenId,
functionCalls: [{
methodName: 'storage_deposit',
args: { account_id: accountId, registration_only: true },
gas: '30000000000000',
amount: FT_MINIMUM_STORAGE_BALANCE_LARGE
}]
});
}

if (!hasStorageOut) {
transactions.unshift({
receiverId: outputTokenId,
functionCalls: [{
methodName: 'storage_deposit',
args: { account_id: accountId, registration_only: true },
gas: '30000000000000',
amount: FT_MINIMUM_STORAGE_BALANCE_LARGE
}]
});
}

return transactions;
} catch (error) {
console.error("Error in swapToken:", error);
throw error;
}
}

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

Example response:
\`\`\`json
{
"inputTokenId": "wrap.testnet",
"outputTokenId": "ref.fakes.testnet",
"amount": "1.5"
}
\`\`\`

{{recentMessages}}

Given the recent messages and wallet information below:

{{walletInfo}}

Extract the following information about the requested token swap:
- Input token ID (the token being sold)
- Output token ID (the token being bought)
- Amount to swap

Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema:
\`\`\`json
{
"inputTokenId": string | null,
"outputTokenId": string | null,
"amount": string | null
}
\`\`\``;

export const executeSwap: Action = {
name: "EXECUTE_SWAP_NEAR",
similes: ["SWAP_TOKENS_NEAR", "TOKEN_SWAP_NEAR", "TRADE_TOKENS_NEAR", "EXCHANGE_TOKENS_NEAR"],
validate: async (runtime: IAgentRuntime, message: Memory) => {
console.log("Message:", message);
return true;
},
description: "Perform a token swap using Ref Finance.",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: { [key: string]: unknown },
callback?: HandlerCallback
): Promise<boolean> => {
// Initialize Ref SDK with testnet environment
init_env(runtime.getSetting("NEAR_NETWORK") || "testnet");
// Compose state
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

const walletInfo = await walletProvider.get(runtime, message, state);
state.walletInfo = walletInfo;

const swapContext = composeContext({
state,
template: swapTemplate,
});

const response = await generateObject({
runtime,
context: swapContext,
modelClass: ModelClass.LARGE,
});

console.log("Response:", response);

if (!response.inputTokenId || !response.outputTokenId || !response.amount) {
console.log("Missing required parameters, skipping swap");
const responseMsg = {
text: "I need the input token ID, output token ID, and amount to perform the swap",
};
callback?.(responseMsg);
return true;
}

try {
// Get account credentials
const accountId = runtime.getSetting("NEAR_ADDRESS");
const secretKey = runtime.getSetting("NEAR_WALLET_SECRET_KEY");

if (!accountId || !secretKey) {
throw new Error("NEAR wallet credentials not configured");
}

// Create keystore and connect to NEAR
const keyStore = new keyStores.InMemoryKeyStore();
const keyPair = utils.KeyPair.fromString(secretKey as KeyPairString);
await keyStore.setKey("testnet", accountId, keyPair);

const nearConnection = await connect({
networkId: runtime.getSetting("NEAR_NETWORK") || "testnet",
keyStore,
nodeUrl: runtime.getSetting("RPC_URL") || "https://rpc.testnet.near.org",
});

// Execute swap
const swapResult = await swapToken(
runtime,
response.inputTokenId,
response.outputTokenId,
response.amount,
Number(runtime.getSetting("SLIPPAGE_TOLERANCE")) || 0.01
);

// Sign and send transactions
const account = await nearConnection.account(accountId);
const results = [];

for (const tx of swapResult) {
for (const functionCall of tx.functionCalls) {
const result = await account.functionCall({
contractId: tx.receiverId,
methodName: functionCall.methodName,
args: functionCall.args,
gas: functionCall.gas,
attachedDeposit: BigInt(functionCall.amount === ONE_YOCTO_NEAR ? '1' : functionCall.amount),
});
results.push(result);
}
}

console.log("Swap completed successfully!");
const txHashes = results.map(r => r.transaction.hash).join(", ");

const responseMsg = {
text: `Swap completed successfully! Transaction hashes: ${txHashes}`,
};

callback?.(responseMsg);
return true;
} catch (error) {
console.error("Error during token swap:", error);
const responseMsg = {
text: `Error during swap: ${error instanceof Error ? error.message : String(error)}`,
};
callback?.(responseMsg);
return false;
}
},
examples: [
[
{
user: "{{user1}}",
content: {
inputTokenId: "wrap.testnet",
outputTokenId: "ref.fakes.testnet",
amount: "1.0",
},
},
{
user: "{{user2}}",
content: {
text: "Swapping 1.0 NEAR for REF...",
action: "TOKEN_SWAP",
},
},
{
user: "{{user2}}",
content: {
text: "Swap completed successfully! Transaction hash: ...",
},
},
],
] as ActionExample[][],
} as Action;
Loading
Loading