Skip to content

Commit

Permalink
feat(plugin-holdstation): add plugin holdstation swap (elizaOS#2596)
Browse files Browse the repository at this point in the history
Co-authored-by: CuongNT <cuongnt@holdstation.com>
Co-authored-by: Sayo <hi@sayo.wtf>
  • Loading branch information
3 people authored and 0xrubusdata committed Jan 21, 2025
1 parent 0ccd0c9 commit 3b08a58
Show file tree
Hide file tree
Showing 20 changed files with 739 additions and 0 deletions.
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 @@ -1091,6 +1092,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

0 comments on commit 3b08a58

Please sign in to comment.