Skip to content

Commit ee25eee

Browse files
feat(plugin-holdstation): add plugin holdstation swap
1 parent 4265be3 commit ee25eee

20 files changed

+739
-0
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@ NEAR_NETWORK=testnet # or mainnet
494494
ZKSYNC_ADDRESS=
495495
ZKSYNC_PRIVATE_KEY=
496496

497+
# HoldStation Wallet Configuration
498+
HOLDSTATION_PRIVATE_KEY=
499+
497500
# Avail DA Configuration
498501
AVAIL_ADDRESS=
499502
AVAIL_SEED=

agent/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"@elizaos/plugin-pyth-data": "workspace:*",
109109
"@elizaos/plugin-openai": "workspace:*",
110110
"@elizaos/plugin-devin": "workspace:*",
111+
"@elizaos/plugin-holdstation": "workspace:*",
111112
"readline": "1.3.0",
112113
"ws": "8.18.0",
113114
"yargs": "17.7.2"

agent/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { agentKitPlugin } from "@elizaos/plugin-agentkit";
1717
import { PrimusAdapter } from "@elizaos/plugin-primus";
1818
import { lightningPlugin } from "@elizaos/plugin-lightning";
1919
import { elizaCodeinPlugin, onchainJson } from "@elizaos/plugin-iq6900";
20+
import { holdstationPlugin } from "@elizaos/plugin-holdstation";
2021

2122
import {
2223
AgentRuntime,
@@ -1092,6 +1093,9 @@ export async function createAgent(
10921093
getSecret(character, "DEVIN_API_TOKEN")
10931094
? devinPlugin
10941095
: null,
1096+
getSecret(character, "HOLDSTATION_PRIVATE_KEY")
1097+
? holdstationPlugin
1098+
: null,
10951099
].filter(Boolean),
10961100
providers: [],
10971101
actions: [],
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*
2+
3+
!dist/**
4+
!package.json
5+
!readme.md
6+
!tsup.config.ts

packages/plugin-holdstation/README.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# @elizaos/plugin-holdstation
2+
3+
Holdstation Wallet Plugin for Eliza
4+
5+
## Features
6+
7+
This plugin provides functionality (now on ZKsync Era, and Berachain coming soon) to:
8+
9+
- Token swapping on hold.so (Holdstation swap)
10+
11+
## Configuration
12+
13+
The plugin requires the following environment variables:
14+
15+
```env
16+
HOLDSTATION_PRIVATE_KEY= # Required: Your wallet's private key
17+
```
18+
19+
## Installation
20+
21+
```bash
22+
pnpm add @elizaos/plugin-holdstation
23+
```
24+
25+
## Development
26+
27+
```bash
28+
pnpm install --no-frozen-lockfile
29+
```
30+
31+
### Building
32+
33+
```bash
34+
pnpm build
35+
```
36+
37+
### Testing
38+
39+
```bash
40+
pnpm test
41+
```
42+
43+
## Credits
44+
45+
Special thanks to:
46+
47+
- The Eliza community for their contributions and feedback
48+
49+
## License
50+
51+
This plugin is part of the Eliza project. See the main project repository for license information.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import eslintGlobalConfig from "../../eslint.config.mjs";
2+
3+
export default [...eslintGlobalConfig];
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@elizaos/plugin-holdstation",
3+
"version": "0.1.1",
4+
"type": "module",
5+
"main": "dist/index.js",
6+
"module": "dist/index.js",
7+
"types": "dist/index.d.ts",
8+
"exports": {
9+
"./package.json": "./package.json",
10+
".": {
11+
"import": {
12+
"@elizaos/source": "./src/index.ts",
13+
"types": "./dist/index.d.ts",
14+
"default": "./dist/index.js"
15+
}
16+
}
17+
},
18+
"dependencies": {
19+
"@elizaos/core": "workspace:*",
20+
"node-cache": "5.1.2",
21+
"viem": "2.22.2"
22+
},
23+
"devDependencies": {
24+
"tsup": "8.3.5",
25+
"@types/node": "^20.0.0"
26+
},
27+
"scripts": {
28+
"build": "tsup --format esm --dts",
29+
"dev": "tsup --format esm --dts --watch",
30+
"lint": "eslint --fix --cache ."
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import {
2+
Action,
3+
IAgentRuntime,
4+
Memory,
5+
HandlerCallback,
6+
State,
7+
composeContext,
8+
ModelClass,
9+
elizaLogger,
10+
ActionExample,
11+
generateObjectDeprecated,
12+
} from "@elizaos/core";
13+
14+
import { swapTemplate } from "../templates";
15+
import { SendTransactionParams, SwapParams } from "../types";
16+
import {
17+
initWalletProvider,
18+
WalletProvider,
19+
} from "../providers/walletProvider";
20+
import { validateHoldStationConfig } from "../environment";
21+
import { HOLDSTATION_ROUTER_ADDRESS, NATIVE_ADDRESS } from "../constants";
22+
import { parseUnits } from "viem";
23+
24+
export class SwapAction {
25+
constructor(private walletProvider: WalletProvider) {}
26+
27+
async swap(params: SwapParams): Promise<any> {
28+
const { items: tokens } = await this.walletProvider.fetchPortfolio();
29+
30+
if (!params.inputTokenCA && !params.inputTokenSymbol) {
31+
throw new Error("Input token not provided");
32+
}
33+
34+
const filters = tokens.filter(
35+
(t) =>
36+
t.symbol === params.inputTokenSymbol ||
37+
t.address === params.inputTokenCA,
38+
);
39+
if (filters.length != 1) {
40+
throw new Error(
41+
"Multiple tokens or no tokens found with the symbol",
42+
);
43+
}
44+
45+
// fill in token info
46+
params.inputTokenCA = filters[0].address;
47+
params.inputTokenSymbol = filters[0].symbol;
48+
const decimals = filters[0].decimals ?? 18;
49+
50+
// parse amount out
51+
const tokenAmount = parseUnits(params.amount.toString(), decimals);
52+
53+
if (!params.outputTokenCA && !params.outputTokenSymbol) {
54+
throw new Error("Output token not provided");
55+
}
56+
57+
if (!params.outputTokenCA || !params.outputTokenSymbol) {
58+
const tokens = await this.walletProvider.fetchAllTokens();
59+
const filters = tokens.filter(
60+
(t) =>
61+
t.symbol === params.outputTokenSymbol ||
62+
t.address === params.outputTokenCA,
63+
);
64+
if (filters.length != 1) {
65+
throw new Error(
66+
"Multiple tokens or no tokens found with the symbol",
67+
);
68+
}
69+
params.outputTokenCA = filters[0].address;
70+
params.outputTokenSymbol = filters[0].symbol;
71+
}
72+
73+
elizaLogger.info("--- Swap params:", params);
74+
75+
// fetch swap tx data
76+
const walletAddress = this.walletProvider.getAddress();
77+
const deadline = Math.floor(Date.now() / 1000) + 10 * 60;
78+
const swapUrl = `https://swap.hold.so/api/swap?src=${params.inputTokenCA}&dst=${params.outputTokenCA}&amount=${tokenAmount}&receiver=${walletAddress}&deadline=${deadline}`;
79+
elizaLogger.info("swapUrl:", swapUrl);
80+
const swapResponse = await fetch(swapUrl);
81+
const swapData = await swapResponse.json();
82+
if (!swapData || swapData.error) {
83+
elizaLogger.error("Swap error:", swapData);
84+
throw new Error(
85+
`Failed to fetch swap: ${swapData?.error || "Unknown error"}`,
86+
);
87+
}
88+
89+
// generate nonce
90+
const nonce = await this.walletProvider
91+
.getPublicClient()
92+
.getTransactionCount({
93+
address: walletAddress,
94+
});
95+
96+
const populatedTx: SendTransactionParams = {
97+
to: HOLDSTATION_ROUTER_ADDRESS,
98+
data: swapData.tx.data,
99+
nonce: nonce,
100+
};
101+
102+
if (
103+
params.inputTokenCA.toLowerCase() !== NATIVE_ADDRESS.toLowerCase()
104+
) {
105+
const allowance = await this.walletProvider.getAllowace(
106+
params.inputTokenCA,
107+
walletAddress,
108+
HOLDSTATION_ROUTER_ADDRESS,
109+
);
110+
if (allowance < tokenAmount) {
111+
await this.walletProvider.approve(
112+
HOLDSTATION_ROUTER_ADDRESS,
113+
params.inputTokenCA,
114+
tokenAmount,
115+
);
116+
}
117+
} else {
118+
populatedTx.value = tokenAmount;
119+
}
120+
121+
const hash = await this.walletProvider.sendTransaction(populatedTx);
122+
123+
return {
124+
hash,
125+
...params,
126+
};
127+
}
128+
}
129+
130+
export const swapAction: Action = {
131+
name: "TOKEN_SWAP_BY_HOLDSTATION",
132+
similes: [
133+
"SWAP_TOKEN",
134+
"SWAP_TOKEN_BY_HOLDSTATION_SWAP",
135+
"EXCHANGE_TOKENS",
136+
"EXCHANGE_TOKENS_BY_HOLDSTATION_SWAP",
137+
"CONVERT_TOKENS",
138+
"CONVERT_TOKENS_BY_HOLDSTATION_SWAP",
139+
],
140+
validate: async (runtime: IAgentRuntime, _message: Memory) => {
141+
await validateHoldStationConfig(runtime);
142+
return true;
143+
},
144+
description: "Perform swapping of tokens on ZKsync by HoldStation swap.",
145+
handler: async (
146+
runtime: IAgentRuntime,
147+
message: Memory,
148+
state: State,
149+
_options: any,
150+
callback: HandlerCallback,
151+
) => {
152+
elizaLogger.log("Starting HoldStation Wallet TOKEN_SWAP handler...");
153+
154+
const walletProvider = await initWalletProvider(runtime);
155+
const action = new SwapAction(walletProvider);
156+
157+
// compose state
158+
if (!state) {
159+
state = (await runtime.composeState(message)) as State;
160+
} else {
161+
state = await runtime.updateRecentMessageState(state);
162+
}
163+
164+
// compose swap context
165+
const swapContext = composeContext({
166+
state,
167+
template: swapTemplate,
168+
});
169+
170+
// generate swap content
171+
const content = await generateObjectDeprecated({
172+
runtime,
173+
context: swapContext,
174+
modelClass: ModelClass.SMALL,
175+
});
176+
177+
elizaLogger.info("generate swap content:", content);
178+
179+
try {
180+
const {
181+
hash,
182+
inputTokenCA,
183+
inputTokenSymbol,
184+
outputTokenCA,
185+
outputTokenSymbol,
186+
amount,
187+
} = await action.swap(content);
188+
189+
elizaLogger.success(
190+
`Swap completed successfully from ${amount} ${inputTokenSymbol} (${inputTokenCA}) to ${outputTokenSymbol} (${outputTokenCA})!\nTransaction Hash: ${hash}`,
191+
);
192+
193+
if (callback) {
194+
callback({
195+
text: `Swap completed successfully from ${amount} ${inputTokenSymbol} (${inputTokenCA}) to ${outputTokenSymbol} (${outputTokenCA})!\nTransaction Hash: ${hash}`,
196+
content: {
197+
success: true,
198+
hash: hash,
199+
},
200+
});
201+
}
202+
return true;
203+
} catch (error) {
204+
elizaLogger.error("Error during token swap:", error);
205+
if (callback) {
206+
callback({
207+
text: `Error during token swap: ${error.message}`,
208+
content: { error: error.message },
209+
});
210+
}
211+
return false;
212+
}
213+
},
214+
examples: [
215+
[
216+
{
217+
user: "{{user1}}",
218+
content: {
219+
text: "Swap 100 USDC for HOLD",
220+
},
221+
},
222+
{
223+
user: "{{agent}}",
224+
content: {
225+
text: "Sure, I'll do swap 100 USDC for HOLD now.",
226+
action: "TOKEN_SWAP",
227+
},
228+
},
229+
{
230+
user: "{{agent}}",
231+
content: {
232+
text: "Swap completed 100 USDC for HOLD successfully! Transaction: ...",
233+
},
234+
},
235+
],
236+
] as ActionExample[][],
237+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const HOLDSTATION_ROUTER_ADDRESS =
2+
"0xD1f1bA4BF2aDe4F47472D0B73ba0f5DC30E225DF";
3+
export const NATIVE_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";

0 commit comments

Comments
 (0)