From 5912a109957ee6d95735a06a755c5a46fc0836fe Mon Sep 17 00:00:00 2001 From: Jay Welsh Date: Tue, 17 Jun 2025 14:11:07 +0200 Subject: [PATCH 1/3] feat(propy): adds first test version of Propy plugin --- .../by-use-case/propy-evm-agent/tsconfig.json | 8 + typescript/packages/plugins/propy/README.md | 47 ++++ .../packages/plugins/propy/package.json | 40 ++++ typescript/packages/plugins/propy/src/abi.ts | 8 + .../packages/plugins/propy/src/index.ts | 2 + .../packages/plugins/propy/src/parameters.ts | 33 +++ .../plugins/propy/src/propy.plugin.ts | 23 ++ .../plugins/propy/src/propy.service.ts | 211 ++++++++++++++++++ .../packages/plugins/propy/src/utils.ts | 36 +++ .../packages/plugins/propy/tsconfig.json | 6 + .../packages/plugins/propy/tsup.config.ts | 6 + typescript/packages/plugins/propy/turbo.json | 11 + 12 files changed, 431 insertions(+) create mode 100644 typescript/examples/by-use-case/propy-evm-agent/tsconfig.json create mode 100644 typescript/packages/plugins/propy/README.md create mode 100644 typescript/packages/plugins/propy/package.json create mode 100644 typescript/packages/plugins/propy/src/abi.ts create mode 100644 typescript/packages/plugins/propy/src/index.ts create mode 100644 typescript/packages/plugins/propy/src/parameters.ts create mode 100644 typescript/packages/plugins/propy/src/propy.plugin.ts create mode 100644 typescript/packages/plugins/propy/src/propy.service.ts create mode 100644 typescript/packages/plugins/propy/src/utils.ts create mode 100644 typescript/packages/plugins/propy/tsconfig.json create mode 100644 typescript/packages/plugins/propy/tsup.config.ts create mode 100644 typescript/packages/plugins/propy/turbo.json diff --git a/typescript/examples/by-use-case/propy-evm-agent/tsconfig.json b/typescript/examples/by-use-case/propy-evm-agent/tsconfig.json new file mode 100644 index 000000000..a7fc4a937 --- /dev/null +++ b/typescript/examples/by-use-case/propy-evm-agent/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["index.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript/packages/plugins/propy/README.md b/typescript/packages/plugins/propy/README.md new file mode 100644 index 000000000..4a2c51868 --- /dev/null +++ b/typescript/packages/plugins/propy/README.md @@ -0,0 +1,47 @@ +
+ + +GOAT + +
+ +# 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 + + diff --git a/typescript/packages/plugins/propy/package.json b/typescript/packages/plugins/propy/package.json new file mode 100644 index 000000000..ec92fe381 --- /dev/null +++ b/typescript/packages/plugins/propy/package.json @@ -0,0 +1,40 @@ +{ + "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:*" + } +} \ No newline at end of file diff --git a/typescript/packages/plugins/propy/src/abi.ts b/typescript/packages/plugins/propy/src/abi.ts new file mode 100644 index 000000000..1c77c3a2a --- /dev/null +++ b/typescript/packages/plugins/propy/src/abi.ts @@ -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)" +]); diff --git a/typescript/packages/plugins/propy/src/index.ts b/typescript/packages/plugins/propy/src/index.ts new file mode 100644 index 000000000..64824d70d --- /dev/null +++ b/typescript/packages/plugins/propy/src/index.ts @@ -0,0 +1,2 @@ +export * from "./propy.plugin"; +export * from "./parameters"; diff --git a/typescript/packages/plugins/propy/src/parameters.ts b/typescript/packages/plugins/propy/src/parameters.ts new file mode 100644 index 000000000..ce042dfda --- /dev/null +++ b/typescript/packages/plugins/propy/src/parameters.ts @@ -0,0 +1,33 @@ +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"), + }), +) {} \ No newline at end of file diff --git a/typescript/packages/plugins/propy/src/propy.plugin.ts b/typescript/packages/plugins/propy/src/propy.plugin.ts new file mode 100644 index 000000000..82fe347ee --- /dev/null +++ b/typescript/packages/plugins/propy/src/propy.plugin.ts @@ -0,0 +1,23 @@ +import { PluginBase, Chain } from "@goat-sdk/core"; +import { PropyService } from "./propy.service"; + +import { mainnet, arbitrum, base, sepolia, baseSepolia } 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 }); +} diff --git a/typescript/packages/plugins/propy/src/propy.service.ts b/typescript/packages/plugins/propy/src/propy.service.ts new file mode 100644 index 000000000..8a4e00cfe --- /dev/null +++ b/typescript/packages/plugins/propy/src/propy.service.ts @@ -0,0 +1,211 @@ +import { Tool } from "@goat-sdk/core"; +import { EVMWalletClient } from "@goat-sdk/wallet-evm"; +import { ethers } from "ethers"; +import BigNumber from 'bignumber.js'; +import { + GetStakingPowerParameters, + GetStakingRemainingLockupParameters, +} from "./parameters"; +import { + STAKING_V3_ABI, +} from "./abi"; +import { + countdownToTimestamp, +} from "./utils"; + +BigNumber.config({ EXPONENTIAL_AT: [-1e+9, 1e+9] }); + +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; + } + + let contractAddress = CONTRACT_ADDRESSES["PRONFTStakingCore"]?.[chainId]; + + if(!contractAddress) { + throw Error(`Failed to fetch balance, unable to detect staking contract address`); + } + + let { + 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} %` + } + } else { + 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; + } + + let { + ownWalletAddress, + specifiedWalletAddress, + selectedStakingModules, + } = parameters; + + let checkModuleConfigs = []; + for(let 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 { + + let response : {}[] = []; + + for(let 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; + } else { + return "Unable to find results" + } + + } catch (error) { + throw Error(`Failed to fetch balance: ${error}`); + } + } +} diff --git a/typescript/packages/plugins/propy/src/utils.ts b/typescript/packages/plugins/propy/src/utils.ts new file mode 100644 index 000000000..8ef3ab1fc --- /dev/null +++ b/typescript/packages/plugins/propy/src/utils.ts @@ -0,0 +1,36 @@ +export const countdownToTimestamp = (unixTimestamp: number, completeText: string, showFullTimer?: boolean): string => { + // Get the current time and calculate the difference + const now = new Date().getTime(); + const diff = (unixTimestamp * 1000) - now; + + // Ensure the timestamp is in the future + if (diff <= 0) { + return completeText; + } + + // Calculate days, hours, minutes, and seconds + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + // Format the countdown string + let countdownString = ""; + if (days > 0) { + countdownString += `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours > 1 ? 's' : ''} `; + if(showFullTimer) { + countdownString += `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds} second${seconds > 1 ? 's' : ''}`; + } + } else if (hours > 0) { + countdownString += `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes > 1 ? 's' : ''} `; + if(showFullTimer) { + countdownString += `${seconds} second${seconds > 1 ? 's' : ''}`; + } + } else if (minutes > 0) { + countdownString += `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds} second${seconds > 1 ? 's' : ''}`; + } else { + countdownString += `${seconds} second${seconds > 1 ? 's' : ''}`; + } + + return countdownString.trim(); +} \ No newline at end of file diff --git a/typescript/packages/plugins/propy/tsconfig.json b/typescript/packages/plugins/propy/tsconfig.json new file mode 100644 index 000000000..b4ae67c1f --- /dev/null +++ b/typescript/packages/plugins/propy/tsconfig.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript/packages/plugins/propy/tsup.config.ts b/typescript/packages/plugins/propy/tsup.config.ts new file mode 100644 index 000000000..2d38789ad --- /dev/null +++ b/typescript/packages/plugins/propy/tsup.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "tsup"; +import { treeShakableConfig } from "../../../tsup.config.base"; + +export default defineConfig({ + ...treeShakableConfig, +}); diff --git a/typescript/packages/plugins/propy/turbo.json b/typescript/packages/plugins/propy/turbo.json new file mode 100644 index 000000000..45f951676 --- /dev/null +++ b/typescript/packages/plugins/propy/turbo.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": ["src/**", "tsup.config.ts", "!./**/*.test.{ts,tsx}", "tsconfig.json"], + "dependsOn": ["^build"], + "outputs": ["dist/**"] + } + } +} From c56e3d698a8f174f3b8412055d5e24bf5926ede1 Mon Sep 17 00:00:00 2001 From: Jay Welsh Date: Tue, 17 Jun 2025 14:26:27 +0200 Subject: [PATCH 2/3] feat(propy): adds first test version of Propy plugin --- README.md | 1 + typescript/.changeset/fuzzy-spoons-smell.md | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 typescript/.changeset/fuzzy-spoons-smell.md diff --git a/README.md b/README.md index cb1825c6b..f301a18c2 100644 --- a/README.md +++ b/README.md @@ -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) | diff --git a/typescript/.changeset/fuzzy-spoons-smell.md b/typescript/.changeset/fuzzy-spoons-smell.md new file mode 100644 index 000000000..509f931a4 --- /dev/null +++ b/typescript/.changeset/fuzzy-spoons-smell.md @@ -0,0 +1,5 @@ +--- +"@goat-sdk/plugin-propy": patch +--- + +Initial foundation for Propy's goat-sdk plugin From 9970098cf29fdecf28a0a9dd56cde6af78ae09b7 Mon Sep 17 00:00:00 2001 From: Jay Welsh Date: Wed, 18 Jun 2025 15:55:55 +0200 Subject: [PATCH 3/3] feat(propy): adds first test version of Propy plugin --- .../packages/plugins/propy/package.json | 14 +- typescript/packages/plugins/propy/src/abi.ts | 2 +- .../packages/plugins/propy/src/parameters.ts | 35 +++- .../plugins/propy/src/propy.plugin.ts | 4 +- .../plugins/propy/src/propy.service.ts | 169 +++++++++--------- .../packages/plugins/propy/src/utils.ts | 62 +++---- 6 files changed, 143 insertions(+), 143 deletions(-) diff --git a/typescript/packages/plugins/propy/package.json b/typescript/packages/plugins/propy/package.json index ec92fe381..7a42dde91 100644 --- a/typescript/packages/plugins/propy/package.json +++ b/typescript/packages/plugins/propy/package.json @@ -1,11 +1,7 @@ { "name": "@goat-sdk/plugin-propy", "version": "0.1.0", - "files": [ - "dist/**/*", - "README.md", - "package.json" - ], + "files": ["dist/**/*", "README.md", "package.json"], "scripts": { "build": "tsup", "clean": "rm -rf dist", @@ -24,11 +20,7 @@ "bugs": { "url": "https://github.com/goat-sdk/goat/issues" }, - "keywords": [ - "ai", - "agents", - "web3" - ], + "keywords": ["ai", "agents", "web3"], "dependencies": { "@goat-sdk/core": "workspace:*", "@goat-sdk/wallet-evm": "workspace:*", @@ -37,4 +29,4 @@ "peerDependencies": { "@goat-sdk/core": "workspace:*" } -} \ No newline at end of file +} diff --git a/typescript/packages/plugins/propy/src/abi.ts b/typescript/packages/plugins/propy/src/abi.ts index 1c77c3a2a..1cd1b1b52 100644 --- a/typescript/packages/plugins/propy/src/abi.ts +++ b/typescript/packages/plugins/propy/src/abi.ts @@ -4,5 +4,5 @@ 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)" + "function lockedAt(address, bytes32) external view returns (uint256)", ]); diff --git a/typescript/packages/plugins/propy/src/parameters.ts b/typescript/packages/plugins/propy/src/parameters.ts index ce042dfda..26c8c5e05 100644 --- a/typescript/packages/plugins/propy/src/parameters.ts +++ b/typescript/packages/plugins/propy/src/parameters.ts @@ -4,21 +4,35 @@ 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"), + 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( + 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([ + ) + .default([ { moduleName: "PRO", }, @@ -27,7 +41,10 @@ export class GetStakingRemainingLockupParameters extends createToolParameters( }, { 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"), + }, + ]) + .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", + ), }), -) {} \ No newline at end of file +) {} diff --git a/typescript/packages/plugins/propy/src/propy.plugin.ts b/typescript/packages/plugins/propy/src/propy.plugin.ts index 82fe347ee..09f634d44 100644 --- a/typescript/packages/plugins/propy/src/propy.plugin.ts +++ b/typescript/packages/plugins/propy/src/propy.plugin.ts @@ -1,7 +1,7 @@ -import { PluginBase, Chain } from "@goat-sdk/core"; +import { Chain, PluginBase } from "@goat-sdk/core"; import { PropyService } from "./propy.service"; -import { mainnet, arbitrum, base, sepolia, baseSepolia } from "viem/chains"; +import { arbitrum, base, baseSepolia, mainnet, sepolia } from "viem/chains"; const SUPPORTED_CHAINS = [mainnet, arbitrum, base, sepolia, baseSepolia]; diff --git a/typescript/packages/plugins/propy/src/propy.service.ts b/typescript/packages/plugins/propy/src/propy.service.ts index 8a4e00cfe..83f801952 100644 --- a/typescript/packages/plugins/propy/src/propy.service.ts +++ b/typescript/packages/plugins/propy/src/propy.service.ts @@ -1,38 +1,31 @@ import { Tool } from "@goat-sdk/core"; import { EVMWalletClient } from "@goat-sdk/wallet-evm"; +import BigNumber from "bignumber.js"; import { ethers } from "ethers"; -import BigNumber from 'bignumber.js'; -import { - GetStakingPowerParameters, - GetStakingRemainingLockupParameters, -} from "./parameters"; -import { - STAKING_V3_ABI, -} from "./abi"; -import { - countdownToTimestamp, -} from "./utils"; - -BigNumber.config({ EXPONENTIAL_AT: [-1e+9, 1e+9] }); - -const CONTRACT_ADDRESSES : {[key: string]: {[key: string]: string}} = { - "PRONFTStakingCore": { +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": { + PropyKeyStakingModule: { 1: "0xBd0969813733df8f506611c204EEF540770CAB72", 11155111: "0xBd0969813733df8f506611c204EEF540770CAB72", }, - "ERC20StakingModule": { + ERC20StakingModule: { 1: "0xF46464ad108B1CC7866DF2Cfa87688F7742BA623", 11155111: "0xF46464ad108B1CC7866DF2Cfa87688F7742BA623", }, - "LPStakingModule": { + LPStakingModule: { 1: "0x8D020131832D8823846232031bD7EEee7A102F2F", 11155111: "0x8D020131832D8823846232031bD7EEee7A102F2F", - } -} + }, +}; export class PropyService { constructor( @@ -42,38 +35,34 @@ export class PropyService { @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", + 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; } - let contractAddress = CONTRACT_ADDRESSES["PRONFTStakingCore"]?.[chainId]; - - if(!contractAddress) { - throw Error(`Failed to fetch balance, unable to detect staking contract address`); + const contractAddress = CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId]; + + if (!contractAddress) { + throw Error("Failed to fetch balance, unable to detect staking contract address"); } - let { - ownWalletAddress, - specifiedWalletAddress, - } = parameters; + const { ownWalletAddress, specifiedWalletAddress } = parameters; let useWalletAddress = ownWalletAddress; - if(specifiedWalletAddress !== ownWalletAddress) { + if (specifiedWalletAddress !== ownWalletAddress) { useWalletAddress = specifiedWalletAddress; } - if(!useWalletAddress) { - throw Error(`Failed to fetch balance, unable to detect wallet address`); + if (!useWalletAddress) { + throw Error("Failed to fetch balance, unable to detect wallet address"); } - - try { + try { const rawBalance = await walletClient.read({ address: contractAddress, abi: STAKING_V3_ABI, @@ -81,7 +70,7 @@ export class PropyService { args: [useWalletAddress], }); - if(Number(rawBalance.value) > 0) { + if (Number(rawBalance.value) > 0) { const rawSupply = await walletClient.read({ address: contractAddress, abi: STAKING_V3_ABI, @@ -89,83 +78,83 @@ export class PropyService { 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} %` - } - } else { + 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": `0 %` - } + 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", + 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) { - + async getStakingRemainingLockupPeriod( + walletClient: EVMWalletClient, + parameters: GetStakingRemainingLockupParameters, + ) { let chainId = walletClient.getChain().id; if (this.chainId) { chainId = this.chainId; } - let { - ownWalletAddress, - specifiedWalletAddress, - selectedStakingModules, - } = parameters; + const { ownWalletAddress, specifiedWalletAddress, selectedStakingModules } = parameters; - let checkModuleConfigs = []; - for(let moduleType of selectedStakingModules) { - if(moduleType.moduleName === 'PRO') { + const checkModuleConfigs = []; + for (const moduleType of selectedStakingModules) { + if (moduleType.moduleName === "PRO") { checkModuleConfigs.push({ assetType: moduleType.moduleName, - contractAddress: CONTRACT_ADDRESSES["PRONFTStakingCore"][chainId], + contractAddress: CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId], moduleId: "0x1eacf06e77941a18f9bc3eb0852750ba87d1f812f0c2df2907082d9904d39335", abi: STAKING_V3_ABI, - }) - } else if (moduleType.moduleName === 'PropyKeys') { + }); + } else if (moduleType.moduleName === "PropyKeys") { checkModuleConfigs.push({ assetType: moduleType.moduleName, - contractAddress: CONTRACT_ADDRESSES["PRONFTStakingCore"][chainId], + contractAddress: CONTRACT_ADDRESSES?.PRONFTStakingCore?.[chainId], moduleId: "0x45078117f79b3fdef93038946c01157f589c320a33e8da6a836521d757382476", abi: STAKING_V3_ABI, - }) - } else if (moduleType.moduleName === 'UniswapLPNFT') { + }); + } else if (moduleType.moduleName === "UniswapLPNFT") { checkModuleConfigs.push({ assetType: moduleType.moduleName, - contractAddress: CONTRACT_ADDRESSES["PRONFTStakingCore"][chainId], + 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`); + + if (checkModuleConfigs.length === 0) { + throw Error("Failed to remaining staking time, unable to detect staking module contract addresses"); } let useWalletAddress = ownWalletAddress; - if(specifiedWalletAddress !== ownWalletAddress) { + if (specifiedWalletAddress !== ownWalletAddress) { useWalletAddress = specifiedWalletAddress; } - if(!useWalletAddress) { - throw Error(`Failed to remaining staking time, unable to detect staker's wallet address`); + if (!useWalletAddress) { + throw Error("Failed to remaining staking time, unable to detect staker's wallet address"); } - - try { - let response : {}[] = []; + try { + const response: { [key: string]: string | Date }[] = []; - for(let moduleConfig of checkModuleConfigs) { + for (const moduleConfig of checkModuleConfigs) { const lockedAt = await walletClient.read({ address: moduleConfig.contractAddress, abi: moduleConfig.abi, @@ -180,30 +169,32 @@ export class PropyService { args: [useWalletAddress, moduleConfig.moduleId], }); - if(Number(lockedAt?.value) > 0 && Number(lockedUntil?.value) > 0) { + if (Number(lockedAt?.value) > 0 && Number(lockedUntil?.value) > 0) { response.push({ - "stakedAssetType": moduleConfig.assetType, + stakedAssetType: moduleConfig.assetType, // "unixTimestampLockedAt": new Date(Number(lockedAt?.value) * 1000), - "dateLockedUntil": new Date(Number(lockedUntil?.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" - }) + 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}`, - }) + 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) { + if (response.length > 0) { return response; - } else { - return "Unable to find results" } - + return "Unable to find results"; } catch (error) { throw Error(`Failed to fetch balance: ${error}`); } diff --git a/typescript/packages/plugins/propy/src/utils.ts b/typescript/packages/plugins/propy/src/utils.ts index 8ef3ab1fc..f1a9517f7 100644 --- a/typescript/packages/plugins/propy/src/utils.ts +++ b/typescript/packages/plugins/propy/src/utils.ts @@ -1,36 +1,36 @@ export const countdownToTimestamp = (unixTimestamp: number, completeText: string, showFullTimer?: boolean): string => { - // Get the current time and calculate the difference - const now = new Date().getTime(); - const diff = (unixTimestamp * 1000) - now; + // Get the current time and calculate the difference + const now = new Date().getTime(); + const diff = unixTimestamp * 1000 - now; - // Ensure the timestamp is in the future - if (diff <= 0) { - return completeText; - } + // Ensure the timestamp is in the future + if (diff <= 0) { + return completeText; + } - // Calculate days, hours, minutes, and seconds - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); + // Calculate days, hours, minutes, and seconds + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); - // Format the countdown string - let countdownString = ""; - if (days > 0) { - countdownString += `${days} day${days > 1 ? 's' : ''} ${hours} hour${hours > 1 ? 's' : ''} `; - if(showFullTimer) { - countdownString += `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds} second${seconds > 1 ? 's' : ''}`; - } - } else if (hours > 0) { - countdownString += `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minute${minutes > 1 ? 's' : ''} `; - if(showFullTimer) { - countdownString += `${seconds} second${seconds > 1 ? 's' : ''}`; - } - } else if (minutes > 0) { - countdownString += `${minutes} minute${minutes > 1 ? 's' : ''} ${seconds} second${seconds > 1 ? 's' : ''}`; - } else { - countdownString += `${seconds} second${seconds > 1 ? 's' : ''}`; - } + // Format the countdown string + let countdownString = ""; + if (days > 0) { + countdownString += `${days} day${days > 1 ? "s" : ""} ${hours} hour${hours > 1 ? "s" : ""} `; + if (showFullTimer) { + countdownString += `${minutes} minute${minutes > 1 ? "s" : ""} ${seconds} second${seconds > 1 ? "s" : ""}`; + } + } else if (hours > 0) { + countdownString += `${hours} hour${hours > 1 ? "s" : ""} ${minutes} minute${minutes > 1 ? "s" : ""} `; + if (showFullTimer) { + countdownString += `${seconds} second${seconds > 1 ? "s" : ""}`; + } + } else if (minutes > 0) { + countdownString += `${minutes} minute${minutes > 1 ? "s" : ""} ${seconds} second${seconds > 1 ? "s" : ""}`; + } else { + countdownString += `${seconds} second${seconds > 1 ? "s" : ""}`; + } - return countdownString.trim(); -} \ No newline at end of file + return countdownString.trim(); +};