Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ GOAT is free software, MIT licensed.
| Orca | Create positions on Orca | [@goat-sdk/plugin-orca](https://www.npmjs.com/package/@goat-sdk/plugin-orca) |
| PlunderSwap | Currency exchange on Zilliqa | [@goat-sdk/plugin-plunderswap](https://www.npmjs.com/package/@goat-sdk/plugin-plunderswap) |
| Polymarket | Bet on Polymarket | [@goat-sdk/plugin-polymarket](https://www.npmjs.com/package/@goat-sdk/plugin-polymarket) |
| Propy | Get updates on staking positions (more coming soon) | [@goat-sdk/plugin-propy](https://www.npmjs.com/package/@goat-sdk/plugin-propy) |
| Pump.fun | Launch a token on Pump.fun | [@goat-sdk/plugin-pump-fun](https://www.npmjs.com/package/@goat-sdk/plugin-pump-fun) |
| Renzo | Create a position on Renzo | [@goat-sdk/plugin-renzo](https://www.npmjs.com/package/@goat-sdk/plugin-renzo) |
| Rugcheck | Check SPL token validity on Rugcheck | [@goat-sdk/plugin-rugcheck](https://www.npmjs.com/package/@goat-sdk/plugin-rugcheck) | [goat-sdk-plugin-rugcheck](https://github.com/goat-sdk/goat/tree/main/python/src/plugins/rugcheck) |
Expand Down
5 changes: 5 additions & 0 deletions typescript/.changeset/fuzzy-spoons-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@goat-sdk/plugin-propy": patch
---

Initial foundation for Propy's goat-sdk plugin
8 changes: 8 additions & 0 deletions typescript/examples/by-use-case/propy-evm-agent/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["index.ts"],
"exclude": ["node_modules", "dist"]
}
47 changes: 47 additions & 0 deletions typescript/packages/plugins/propy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<div align="center">
<a href="https://github.com/goat-sdk/goat">

<img src="https://github.com/user-attachments/assets/5fc7f121-259c-492c-8bca-f15fe7eb830c" alt="GOAT" width="100px" height="auto" style="object-fit: contain;">
</a>
</div>

# Propy GOAT Plugin

Propy's experimental AI agent to make it easier for new blockchain users to read data from the blockchain.

## Installation
```bash
npm install @goat-sdk/plugin-propy
yarn add @goat-sdk/plugin-propy
pnpm add @goat-sdk/plugin-propy
```

## Usage
```typescript
import { propy } from '@goat-sdk/plugin-propy';

const tools = await getOnChainTools({
wallet: // ...
plugins: [
propy({
chainId: sepolia.id,
provider: process.env.RPC_PROVIDER_URL,
})
]
});
```

## Tools
* get_staking_power
* get_staking_remaining_lockup_period
* MORE COMING SOON

<footer>
<br/>
<br/>
<div>
<a href="https://github.com/goat-sdk/goat">
<img src="https://github.com/user-attachments/assets/59fa5ddc-9d47-4d41-a51a-64f6798f94bd" alt="GOAT" width="100%" height="auto" style="object-fit: contain; max-width: 800px;">
</a>
</div>
</footer>
32 changes: 32 additions & 0 deletions typescript/packages/plugins/propy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@goat-sdk/plugin-propy",
"version": "0.1.0",
"files": ["dist/**/*", "README.md", "package.json"],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "vitest run --passWithNoTests"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false,
"homepage": "https://ohmygoat.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/goat-sdk/goat.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/goat-sdk/goat/issues"
},
"keywords": ["ai", "agents", "web3"],
"dependencies": {
"@goat-sdk/core": "workspace:*",
"@goat-sdk/wallet-evm": "workspace:*",
"zod": "catalog:"
},
"peerDependencies": {
"@goat-sdk/core": "workspace:*"
}
}
8 changes: 8 additions & 0 deletions typescript/packages/plugins/propy/src/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { parseAbi } from "viem";

export const STAKING_V3_ABI = parseAbi([
"function balanceOf(address account) external view returns (uint256)",
"function totalSupply() external view returns (uint256)",
"function locked(address, bytes32) external view returns (uint256)",
"function lockedAt(address, bytes32) external view returns (uint256)",
]);
2 changes: 2 additions & 0 deletions typescript/packages/plugins/propy/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./propy.plugin";
export * from "./parameters";
50 changes: 50 additions & 0 deletions typescript/packages/plugins/propy/src/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createToolParameters } from "@goat-sdk/core";
import { z } from "zod";

export class GetStakingPowerParameters extends createToolParameters(
z.object({
ownWalletAddress: z.string().describe("The users own wallet address"),
specifiedWalletAddress: z
.string()
.describe("A different wallet address to ownWalletAddress, explicitly specified within the user's prompt"),
hasSpecifiedWalletAddress: z
.boolean()
.describe(
"Whether or not the user has explicitly specified a wallet address, should be false if ownWalletAddress and specifiedWalletAddress match each other",
),
}),
) {}

export class GetStakingRemainingLockupParameters extends createToolParameters(
z.object({
ownWalletAddress: z.string().describe("The users own wallet address"),
specifiedWalletAddress: z
.string()
.describe("A different wallet address to ownWalletAddress, explicitly specified within the user's prompt"),
hasSpecifiedWalletAddress: z
.boolean()
.describe(
"Whether or not the user has explicitly specified a wallet address, should be false if ownWalletAddress and specifiedWalletAddress match each other",
),
selectedStakingModules: z
.array(
z.object({
moduleName: z.enum(["PRO", "PropyKeys", "UniswapLPNFT"]),
}),
)
.default([
{
moduleName: "PRO",
},
{
moduleName: "PropyKeys",
},
{
moduleName: "UniswapLPNFT",
},
])
.describe(
"The user-specified modules that they want to check the remaining lockup period of, usually we would check for all modules but if they ask about the lockup associated with a particular asset, we will just check for the lockups associated with the specified assets, multiple modules can be specified",
),
}),
) {}
23 changes: 23 additions & 0 deletions typescript/packages/plugins/propy/src/propy.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Chain, PluginBase } from "@goat-sdk/core";
import { PropyService } from "./propy.service";

import { arbitrum, base, baseSepolia, mainnet, sepolia } from "viem/chains";

const SUPPORTED_CHAINS = [mainnet, arbitrum, base, sepolia, baseSepolia];

type PropyPluginOptions = {
provider?: string;
chainId?: number;
};

export class PropyPlugin extends PluginBase {
constructor(options: PropyPluginOptions) {
super("propy", [new PropyService(options.provider, options.chainId)]);
}

supportsChain = (chain: Chain) => chain.type === "evm" && SUPPORTED_CHAINS.some((c) => c.id === chain.id);
}

export function propy({ provider, chainId }: PropyPluginOptions) {
return new PropyPlugin({ provider, chainId });
}
202 changes: 202 additions & 0 deletions typescript/packages/plugins/propy/src/propy.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { Tool } from "@goat-sdk/core";
import { EVMWalletClient } from "@goat-sdk/wallet-evm";
import BigNumber from "bignumber.js";
import { ethers } from "ethers";
import { STAKING_V3_ABI } from "./abi";
import { GetStakingPowerParameters, GetStakingRemainingLockupParameters } from "./parameters";
import { countdownToTimestamp } from "./utils";

BigNumber.config({ EXPONENTIAL_AT: [-1e9, 1e9] });

const CONTRACT_ADDRESSES: { [key: string]: { [key: string]: string } } = {
PRONFTStakingCore: {
1: "0x4e2f246042FC67d8173397c01775Fc29508c9aCe",
11155111: "0xea6fFe0d13eca58CfF3427d65807338982BdC687",
},
PropyKeyStakingModule: {
1: "0xBd0969813733df8f506611c204EEF540770CAB72",
11155111: "0xBd0969813733df8f506611c204EEF540770CAB72",
},
ERC20StakingModule: {
1: "0xF46464ad108B1CC7866DF2Cfa87688F7742BA623",
11155111: "0xF46464ad108B1CC7866DF2Cfa87688F7742BA623",
},
LPStakingModule: {
1: "0x8D020131832D8823846232031bD7EEee7A102F2F",
11155111: "0x8D020131832D8823846232031bD7EEee7A102F2F",
},
};

export class PropyService {
constructor(
private readonly provider: string | undefined,
private readonly chainId: number | undefined,
) {}

@Tool({
name: "get_staking_power",
description:
"Get the current staking power (pSTAKE balance) of the current wallet address, and it's share of incoming rewards",
})
async getStakingPower(walletClient: EVMWalletClient, parameters: GetStakingPowerParameters) {
let chainId = walletClient.getChain().id;

if (this.chainId) {
chainId = this.chainId;
}

const contractAddress = CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId];

if (!contractAddress) {
throw Error("Failed to fetch balance, unable to detect staking contract address");
}

const { ownWalletAddress, specifiedWalletAddress } = parameters;

let useWalletAddress = ownWalletAddress;
if (specifiedWalletAddress !== ownWalletAddress) {
useWalletAddress = specifiedWalletAddress;
}

if (!useWalletAddress) {
throw Error("Failed to fetch balance, unable to detect wallet address");
}

try {
const rawBalance = await walletClient.read({
address: contractAddress,
abi: STAKING_V3_ABI,
functionName: "balanceOf",
args: [useWalletAddress],
});

if (Number(rawBalance.value) > 0) {
const rawSupply = await walletClient.read({
address: contractAddress,
abi: STAKING_V3_ABI,
functionName: "totalSupply",
args: [],
});

const percentageShare = new BigNumber(`${rawBalance.value}`)
.multipliedBy(100)
.dividedBy(new BigNumber(`${rawSupply.value}`))
.toFixed(2);
return {
stakingPower: `${Number(ethers.utils.formatUnits(`${rawBalance.value}`, 8)).toFixed(2)} pSTAKE`,
percentageShareOfIncomingRewards: `${percentageShare} %`,
};
}
return {
stakingPower: `${Number(ethers.utils.formatUnits(`${rawBalance.value}`, 8)).toFixed(2)} pSTAKE`,
percentageShareOfIncomingRewards: "0 %",
};
} catch (error) {
throw Error(`Failed to fetch balance: ${error}`);
}
}
@Tool({
name: "get_staking_remaining_lockup_period",
description:
"Get the remaining staking lockup periods of the current wallet address, support the following staking modules: PropyKey Staking, PRO staking & Uniswap Liquidity Provider NFT (LP NFT) staking",
})
async getStakingRemainingLockupPeriod(
walletClient: EVMWalletClient,
parameters: GetStakingRemainingLockupParameters,
) {
let chainId = walletClient.getChain().id;

if (this.chainId) {
chainId = this.chainId;
}

const { ownWalletAddress, specifiedWalletAddress, selectedStakingModules } = parameters;

const checkModuleConfigs = [];
for (const moduleType of selectedStakingModules) {
if (moduleType.moduleName === "PRO") {
checkModuleConfigs.push({
assetType: moduleType.moduleName,
contractAddress: CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId],
moduleId: "0x1eacf06e77941a18f9bc3eb0852750ba87d1f812f0c2df2907082d9904d39335",
abi: STAKING_V3_ABI,
});
} else if (moduleType.moduleName === "PropyKeys") {
checkModuleConfigs.push({
assetType: moduleType.moduleName,
contractAddress: CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId],
moduleId: "0x45078117f79b3fdef93038946c01157f589c320a33e8da6a836521d757382476",
abi: STAKING_V3_ABI,
});
} else if (moduleType.moduleName === "UniswapLPNFT") {
checkModuleConfigs.push({
assetType: moduleType.moduleName,
contractAddress: CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId],
moduleId: "0xc857a6e7be06cf7940500da1c03716d761f264c09e870f16bef249a1d84f00ac",
abi: STAKING_V3_ABI,
});
}
}

if (checkModuleConfigs.length === 0) {
throw Error("Failed to remaining staking time, unable to detect staking module contract addresses");
}

let useWalletAddress = ownWalletAddress;
if (specifiedWalletAddress !== ownWalletAddress) {
useWalletAddress = specifiedWalletAddress;
}

if (!useWalletAddress) {
throw Error("Failed to remaining staking time, unable to detect staker's wallet address");
}

try {
const response: { [key: string]: string | Date }[] = [];

for (const moduleConfig of checkModuleConfigs) {
const lockedAt = await walletClient.read({
address: moduleConfig.contractAddress,
abi: moduleConfig.abi,
functionName: "lockedAt",
args: [useWalletAddress, moduleConfig.moduleId],
});

const lockedUntil = await walletClient.read({
address: moduleConfig.contractAddress,
abi: moduleConfig.abi,
functionName: "locked",
args: [useWalletAddress, moduleConfig.moduleId],
});

if (Number(lockedAt?.value) > 0 && Number(lockedUntil?.value) > 0) {
response.push({
stakedAssetType: moduleConfig.assetType,
// "unixTimestampLockedAt": new Date(Number(lockedAt?.value) * 1000),
dateLockedUntil: new Date(Number(lockedUntil?.value) * 1000),
// "currentDate": new Date(),
timeRemainingOnLockup: countdownToTimestamp(
Number(lockedUntil?.value),
"Lockup period is complete",
false,
),
meta: "Include an intuitive amount of time left for the staking (e.g. in days/hours/minutes, use appropriate values), also mention the start and end date of the staking",
});
} else {
response.push({
stakedAssetType: moduleConfig.assetType,
unixTimestampLockedAt: `No stake "locked at" time, this indicates that the current wallet address doesn't have an active staking position for ${moduleConfig.assetType}`,
unixTimestampLockedUntil: `No stake "locked until" time, this indicates that the current wallet address doesn't have an active staking position for ${moduleConfig.assetType}`,
});
}
}

if (response.length > 0) {
return response;
}
return "Unable to find results";
} catch (error) {
throw Error(`Failed to fetch balance: ${error}`);
}
}
}
Loading
Loading