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(plugin-holdstation): add plugin holdstation swap #2596

Merged
merged 2 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,9 @@ NEAR_NETWORK=testnet # or mainnet
ZKSYNC_ADDRESS=
ZKSYNC_PRIVATE_KEY=

# HoldStation Wallet Configuration
HOLDSTATION_PRIVATE_KEY=

# Avail DA Configuration
AVAIL_ADDRESS=
AVAIL_SEED=
Expand Down
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"@elizaos/plugin-pyth-data": "workspace:*",
"@elizaos/plugin-openai": "workspace:*",
"@elizaos/plugin-devin": "workspace:*",
"@elizaos/plugin-holdstation": "workspace:*",
"readline": "1.3.0",
"ws": "8.18.0",
"yargs": "17.7.2"
Expand Down
4 changes: 4 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { agentKitPlugin } from "@elizaos/plugin-agentkit";
import { PrimusAdapter } from "@elizaos/plugin-primus";
import { lightningPlugin } from "@elizaos/plugin-lightning";
import { elizaCodeinPlugin, onchainJson } from "@elizaos/plugin-iq6900";
import { holdstationPlugin } from "@elizaos/plugin-holdstation";

import {
AgentRuntime,
Expand Down Expand Up @@ -1092,6 +1093,9 @@ export async function createAgent(
getSecret(character, "DEVIN_API_TOKEN")
? devinPlugin
: null,
getSecret(character, "HOLDSTATION_PRIVATE_KEY")
? holdstationPlugin
: null,
].filter(Boolean),
providers: [],
actions: [],
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-holdstation/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
51 changes: 51 additions & 0 deletions packages/plugin-holdstation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# @elizaos/plugin-holdstation

Holdstation Wallet Plugin for Eliza

## Features

This plugin provides functionality (now on ZKsync Era, and Berachain coming soon) to:

- Token swapping on hold.so (Holdstation swap)

## Configuration

The plugin requires the following environment variables:

```env
HOLDSTATION_PRIVATE_KEY= # Required: Your wallet's private key
```

## Installation

```bash
pnpm add @elizaos/plugin-holdstation
```

## Development

```bash
pnpm install --no-frozen-lockfile
```

### Building

```bash
pnpm build
```

### Testing

```bash
pnpm test
```

## Credits

Special thanks to:

- The Eliza community for their contributions and feedback

## License

This plugin is part of the Eliza project. See the main project repository for license information.
3 changes: 3 additions & 0 deletions packages/plugin-holdstation/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [...eslintGlobalConfig];
32 changes: 32 additions & 0 deletions packages/plugin-holdstation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@elizaos/plugin-holdstation",
"version": "0.1.1",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"@elizaos/source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"dependencies": {
"@elizaos/core": "workspace:*",
"node-cache": "5.1.2",
"viem": "2.22.2"
},
"devDependencies": {
"tsup": "8.3.5",
"@types/node": "^20.0.0"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"lint": "eslint --fix --cache ."
}
}
237 changes: 237 additions & 0 deletions packages/plugin-holdstation/src/actions/swapAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import {
Action,
IAgentRuntime,
Memory,
HandlerCallback,
State,
composeContext,
ModelClass,
elizaLogger,
ActionExample,
generateObjectDeprecated,
} from "@elizaos/core";

import { swapTemplate } from "../templates";
import { SendTransactionParams, SwapParams } from "../types";
import {
initWalletProvider,
WalletProvider,
} from "../providers/walletProvider";
import { validateHoldStationConfig } from "../environment";
import { HOLDSTATION_ROUTER_ADDRESS, NATIVE_ADDRESS } from "../constants";
import { parseUnits } from "viem";

export class SwapAction {
constructor(private walletProvider: WalletProvider) {}

async swap(params: SwapParams): Promise<any> {
const { items: tokens } = await this.walletProvider.fetchPortfolio();

if (!params.inputTokenCA && !params.inputTokenSymbol) {
throw new Error("Input token not provided");
}

const filters = tokens.filter(
(t) =>
t.symbol === params.inputTokenSymbol ||
t.address === params.inputTokenCA,
);
if (filters.length != 1) {
throw new Error(
"Multiple tokens or no tokens found with the symbol",
);
}

// fill in token info
params.inputTokenCA = filters[0].address;
params.inputTokenSymbol = filters[0].symbol;
const decimals = filters[0].decimals ?? 18;

// parse amount out
const tokenAmount = parseUnits(params.amount.toString(), decimals);

if (!params.outputTokenCA && !params.outputTokenSymbol) {
throw new Error("Output token not provided");
}

if (!params.outputTokenCA || !params.outputTokenSymbol) {
const tokens = await this.walletProvider.fetchAllTokens();
const filters = tokens.filter(
(t) =>
t.symbol === params.outputTokenSymbol ||
t.address === params.outputTokenCA,
);
if (filters.length != 1) {
throw new Error(
"Multiple tokens or no tokens found with the symbol",
);
}
params.outputTokenCA = filters[0].address;
params.outputTokenSymbol = filters[0].symbol;
}

elizaLogger.info("--- Swap params:", params);

// fetch swap tx data
const walletAddress = this.walletProvider.getAddress();
const deadline = Math.floor(Date.now() / 1000) + 10 * 60;
const swapUrl = `https://swap.hold.so/api/swap?src=${params.inputTokenCA}&dst=${params.outputTokenCA}&amount=${tokenAmount}&receiver=${walletAddress}&deadline=${deadline}`;
elizaLogger.info("swapUrl:", swapUrl);
const swapResponse = await fetch(swapUrl);
const swapData = await swapResponse.json();
if (!swapData || swapData.error) {
elizaLogger.error("Swap error:", swapData);
throw new Error(
`Failed to fetch swap: ${swapData?.error || "Unknown error"}`,
);
}

// generate nonce
const nonce = await this.walletProvider
.getPublicClient()
.getTransactionCount({
address: walletAddress,
});

const populatedTx: SendTransactionParams = {
to: HOLDSTATION_ROUTER_ADDRESS,
data: swapData.tx.data,
nonce: nonce,
};

if (
params.inputTokenCA.toLowerCase() !== NATIVE_ADDRESS.toLowerCase()
) {
const allowance = await this.walletProvider.getAllowace(
params.inputTokenCA,
walletAddress,
HOLDSTATION_ROUTER_ADDRESS,
);
if (allowance < tokenAmount) {
await this.walletProvider.approve(
HOLDSTATION_ROUTER_ADDRESS,
params.inputTokenCA,
tokenAmount,
);
}
} else {
populatedTx.value = tokenAmount;
}

const hash = await this.walletProvider.sendTransaction(populatedTx);

return {
hash,
...params,
};
}
}

export const swapAction: Action = {
name: "TOKEN_SWAP_BY_HOLDSTATION",
similes: [
"SWAP_TOKEN",
"SWAP_TOKEN_BY_HOLDSTATION_SWAP",
"EXCHANGE_TOKENS",
"EXCHANGE_TOKENS_BY_HOLDSTATION_SWAP",
"CONVERT_TOKENS",
"CONVERT_TOKENS_BY_HOLDSTATION_SWAP",
],
validate: async (runtime: IAgentRuntime, _message: Memory) => {
await validateHoldStationConfig(runtime);
return true;
},
description: "Perform swapping of tokens on ZKsync by HoldStation swap.",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
_options: any,
callback: HandlerCallback,
) => {
elizaLogger.log("Starting HoldStation Wallet TOKEN_SWAP handler...");

const walletProvider = await initWalletProvider(runtime);
const action = new SwapAction(walletProvider);

// compose state
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

// compose swap context
const swapContext = composeContext({
state,
template: swapTemplate,
});

// generate swap content
const content = await generateObjectDeprecated({
runtime,
context: swapContext,
modelClass: ModelClass.SMALL,
});

elizaLogger.info("generate swap content:", content);

try {
const {
hash,
inputTokenCA,
inputTokenSymbol,
outputTokenCA,
outputTokenSymbol,
amount,
} = await action.swap(content);

elizaLogger.success(
`Swap completed successfully from ${amount} ${inputTokenSymbol} (${inputTokenCA}) to ${outputTokenSymbol} (${outputTokenCA})!\nTransaction Hash: ${hash}`,
);

if (callback) {
callback({
text: `Swap completed successfully from ${amount} ${inputTokenSymbol} (${inputTokenCA}) to ${outputTokenSymbol} (${outputTokenCA})!\nTransaction Hash: ${hash}`,
content: {
success: true,
hash: hash,
},
});
}
return true;
} catch (error) {
elizaLogger.error("Error during token swap:", error);
if (callback) {
callback({
text: `Error during token swap: ${error.message}`,
content: { error: error.message },
});
}
return false;
}
},
examples: [
[
{
user: "{{user1}}",
content: {
text: "Swap 100 USDC for HOLD",
},
},
{
user: "{{agent}}",
content: {
text: "Sure, I'll do swap 100 USDC for HOLD now.",
action: "TOKEN_SWAP",
},
},
{
user: "{{agent}}",
content: {
text: "Swap completed 100 USDC for HOLD successfully! Transaction: ...",
},
},
],
] as ActionExample[][],
};
3 changes: 3 additions & 0 deletions packages/plugin-holdstation/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const HOLDSTATION_ROUTER_ADDRESS =
"0xD1f1bA4BF2aDe4F47472D0B73ba0f5DC30E225DF";
export const NATIVE_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
Loading
Loading