Skip to content

Commit

Permalink
Add Comoswallet, cosmosbank module and cosmos langchain example (#222)
Browse files Browse the repository at this point in the history
* Update Chain.ts, with feature CosmosChain

* add cosmos wallet

* add cosmosbank module plugin

* add cosmos langchian example

* Update bank.service.ts

* Delete typescript/examples/langchain/cosmos/CHANGELOG.md

* Delete typescript/packages/plugins/cosmosbank/CHANGELOG.md

* Delete typescript/packages/wallets/cosmos/CHANGELOG.md

* Fix PR

---------

Co-authored-by: Agus <[email protected]>
  • Loading branch information
lukrycyfa and 0xaguspunk authored Jan 16, 2025
1 parent e8c0e49 commit a6970f8
Show file tree
Hide file tree
Showing 24 changed files with 1,001 additions and 10 deletions.
15 changes: 15 additions & 0 deletions typescript/examples/langchain/cosmos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Langchain with cosmos Example using cohere-ai.

## Setup

Copy the `.env.template` and populate with your values.

```
cp .env.template .env
```

## Usage

```
npx ts-node index.ts
```
3 changes: 3 additions & 0 deletions typescript/examples/langchain/cosmos/env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
COHERE_API_KEY=
WALLET_MNEMONICS=
RPC_PROVIDER_URL=
60 changes: 60 additions & 0 deletions typescript/examples/langchain/cosmos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ChatCohere } from "@langchain/cohere";
import type { ChatPromptTemplate } from "@langchain/core/prompts";
import { AgentExecutor, createStructuredChatAgent } from "langchain/agents";
import { pull } from "langchain/hub";

import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { SigningStargateClient } from "@cosmjs/stargate";

import { getOnChainTools } from "@goat-sdk/adapter-langchain";
import { cosmosbank } from "@goat-sdk/plugin-cosmosbank";
import { CosmosWalletOptions, cosmos } from "@goat-sdk/wallet-cosmos";

require("dotenv").config();

(async (): Promise<void> => {
const mnemonic = process.env.WALLET_MNEMONICS as `0x${string}`;
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(
mnemonic,
{ prefix: "pryzm" }, // e.g atom, pryzm, pokt, dora e.t.c
);

const [Account] = await wallet.getAccounts();
const rpcEndpoint = process.env.RPC_PROVIDER_URL as `0x${string}`;
const client = await SigningStargateClient.connectWithSigner(rpcEndpoint, wallet);

const walletClient: CosmosWalletOptions = {
client: client,
account: Account,
};

const llm = new ChatCohere({
model: "command-r-plus",
});

const prompt = await pull<ChatPromptTemplate>("hwchase17/structured-chat-agent");

const tools = await getOnChainTools({
wallet: cosmos(walletClient),
plugins: [await cosmosbank()],
});

const agent = await createStructuredChatAgent({
llm,
tools,
prompt,
});

const agentExecutor = new AgentExecutor({
agent,
tools,
});

const response = await agentExecutor.invoke({
//input: "get the {{PRYZM}} token total supply",
//input: "get the {{PRYZM}} token balance of this address {{_addr}}",
//input: "send 1 {{PRYZM}} token to {{_addr}}",
});

console.log(response);
})();
23 changes: 23 additions & 0 deletions typescript/examples/langchain/cosmos/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "goat-examples-langchain-cosmos",
"version": "0.0.1",
"description": "",
"private": true,
"scripts": {
"test": "vitest run --passWithNoTests"
},
"author": "",
"license": "MIT",
"dependencies": {
"@goat-sdk/adapter-langchain": "workspace:*",
"@goat-sdk/plugin-cosmosbank": "workspace:*",
"@goat-sdk/wallet-cosmos": "workspace:*",
"@goat-sdk/core": "workspace:*",
"@cosmjs/stargate": "^0.32.4",
"@langchain/core": "catalog:",
"@langchain/cohere": "^0.3.2",
"@cosmjs/proto-signing": "^0.32.4",
"dotenv": "^16.4.5",
"langchain": "^0.3.2"
}
}
8 changes: 8 additions & 0 deletions typescript/examples/langchain/cosmos/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"]
}
14 changes: 13 additions & 1 deletion typescript/packages/core/src/types/Chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
* @param type - "evm" or "solana", extend this union as needed (e.g., "sui")
* @param id - Chain ID, optional for EVM
*/
export type Chain = EvmChain | SolanaChain | AptosChain | ChromiaChain | FuelChain | SuiChain | ZilliqaChain;
export type Chain =
| EvmChain
| SolanaChain
| AptosChain
| ChromiaChain
| FuelChain
| SuiChain
| ZilliqaChain
| CosmosChain;

export type SuiChain = {
type: "sui";
Expand Down Expand Up @@ -34,3 +42,7 @@ export type ZilliqaChain = {
id: number;
evmId: number;
};

export type CosmosChain = {
type: "cosmos";
};
31 changes: 31 additions & 0 deletions typescript/packages/plugins/cosmosbank/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Goat CosmosBank Plugin 🐐 - TypeScript

CosmosBank plugin for [Goat 🐐](https://ohmygoat.dev). Allows you to create tools for interacting with the bank module on cosmos chains.

## Installation
```
npm install @goat-sdk/plugin-cosmosbank
```

## Usage

```typescript
import { cosmosbank } from "@goat-sdk/plugin-cosmosbank";


const plugin = cosmosbank();
```

## Working example

See the [LangChain example](https://github.com/goat-sdk/goat/tree/main/typescript/examples/langchain/cosmos) for a working example of how to use the CosmosBank plugin.

## Goat

<div align="center">
Go out and eat some grass.

[Docs](https://ohmygoat.dev) | [Examples](https://github.com/goat-sdk/goat/tree/main/typescript/examples) | [Discord](https://discord.gg/2F8zTVnnFz)</div>

## Goat 🐐
Goat 🐐 (Great Onchain Agent Toolkit) is an open-source library enabling AI agents to interact with blockchain protocols and smart contracts via their own wallets.
47 changes: 47 additions & 0 deletions typescript/packages/plugins/cosmosbank/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@goat-sdk/plugin-cosmosbank",
"version": "0.0.1",
"files": ["dist/**/*", "README.md", "package.json"],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "vitest run --passWithNoTests"
},
"sideEffects": false,
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",

"dependencies": {
"@goat-sdk/core": "workspace:*",
"@goat-sdk/wallet-cosmos": "workspace:*",
"@cosmjs/stargate": "^0.32.4",
"cosmjs-types": "^0.9.0",
"chain-registry": "^1.69.74",
"zod": "catalog:"
},

"peerDependencies": {
"@goat-sdk/core": "workspace:*",
"@cosmjs/stargate": "^0.32.4",
"cosmjs-types": "^0.9.0"
},

"engines": {
"node": ">=20.12.2 <21",
"npm": "please-use-pnpm",
"pnpm": ">=9",
"yarn": "please-use-pnpm"
},
"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"],
"packageManager": "[email protected]"
}
15 changes: 15 additions & 0 deletions typescript/packages/plugins/cosmosbank/src/bank.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Chain, PluginBase } from "@goat-sdk/core";
import { CosmosClient } from "@goat-sdk/wallet-cosmos";
import { BankService } from "./bank.service";

export class BANKPlugin extends PluginBase<CosmosClient> {
constructor() {
super("cosmosbank", [new BankService()]);
}

supportsChain = (chain: Chain) => chain.type === "cosmos";
}

export async function cosmosbank() {
return new BANKPlugin();
}
155 changes: 155 additions & 0 deletions typescript/packages/plugins/cosmosbank/src/bank.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Tool } from "@goat-sdk/core";
import { CosmosClient } from "@goat-sdk/wallet-cosmos";
import { assets, chains } from "chain-registry";
import {
QueryBalanceRequest,
QueryBalanceResponse,
QueryDenomMetadataRequest,
QueryDenomMetadataResponse,
QuerySupplyOfRequest,
QuerySupplyOfResponse,
} from "cosmjs-types/cosmos/bank/v1beta1/query";
import {
denomMetadataParametersSchema,
getBalanceParametersSchema,
sendTokenObjectParametersSchema,
supplyOfParametersSchema,
} from "./parameters";

export class BankService {
@Tool({
description: "Gets the balance of a token denom in base units. Convert to decimal units before returning.",
})
async tokenBalance(walletClient: CosmosClient, parameters: getBalanceParametersSchema) {
try {
const t = await this.getChainInfo(walletClient);
const _d = t.asset?.assets.find((a) => a.symbol === parameters.symbol);

if (!_d) throw new Error("the Requested Token is unavailabe on the network");

const data = QueryBalanceRequest.encode({ address: parameters.address, denom: _d?.base }).finish();
const message = { typeUrl: "/cosmos.bank.v1beta1.Query/Balance", value: data };

const rawBalance = await walletClient.read({ message });
const decode = QueryBalanceResponse.decode(rawBalance.value.value);

if (!decode?.balance) throw new Error("the requested balance is unavailable");

const ex = t.asset?.assets.find((a) => a.base === decode.balance?.denom);
const xp = ex?.denom_units.find((d) => d.denom === ex?.display)?.exponent ?? 0;
const bal = await this.convertFromBaseUnit(Number(decode.balance.amount), xp);
return bal.toString();
} catch (error) {
throw Error(`Failed to fetch balance: ${error}`);
}
}

@Tool({
description: "Get the metadata of a token with the specified symbol.",
})
async demonMetada(walletClient: CosmosClient, parameters: denomMetadataParametersSchema) {
try {
const _d = (await this.getChainInfo(walletClient)).asset?.assets.find(
(a) => a.symbol === parameters.symbol,
);

if (!_d) throw new Error("the Requested Token is unavailabe on the network");

const data = QueryDenomMetadataRequest.encode({ denom: _d?.base }).finish();
const message = { typeUrl: "/cosmos.bank.v1beta1.Query/DenomMetadata", value: data };

const metadata = await walletClient.read({ message });
const decode = QueryDenomMetadataResponse.decode(metadata.value.value);

if (!decode.metadata) throw new Error("the requested metadata is unavailable");

return `${decode.metadata.display}-${decode.metadata.name}-${decode.metadata.description}
-${decode.metadata.symbol}`;
} catch (error) {
throw Error(`Failed to fetch denom metadata: ${error}`);
}
}

@Tool({
description: "Get the total supply of a token with the specified symbol.",
})
async supplyOf(walletClient: CosmosClient, parameters: supplyOfParametersSchema) {
try {
const t = await this.getChainInfo(walletClient);
const _d = t.asset?.assets.find((a) => a.symbol === parameters.symbol);

if (!_d) throw new Error("the Requested Token is unavailabe on the network");

const data = QuerySupplyOfRequest.encode({ denom: _d?.base }).finish();
const message = { typeUrl: "/cosmos.bank.v1beta1.Query/SupplyOf", value: data };

const supplyof = await walletClient.read({ message });
const decode = QuerySupplyOfResponse.decode(supplyof.value.value);

if (!decode.amount) throw new Error("the requested token data is unavailable");

const ex = t.asset?.assets.find((a) => a.base === decode.amount?.denom);
const xp = ex?.denom_units.find((d) => d.denom === ex?.display)?.exponent ?? 0;
const total = await this.convertFromBaseUnit(Number(decode.amount.amount), xp);
return `$${total.toString()}_${decode.amount.denom}`;
} catch (error) {
throw Error(`Failed to fetch total supply: ${error}`);
}
}

@Tool({
description: "Sends an amount of a Token of a specified symbol to a receivers address.",
})
async sendToken(walletClient: CosmosClient, parameters: sendTokenObjectParametersSchema) {
try {
const t = await this.getChainInfo(walletClient);
const _d = t.asset?.assets.find((a) => a.symbol === parameters.amount.symbol);

if (!_d) throw new Error("the Requested Token is unavailabe on the network");

const ex = t.asset?.assets.find((a) => a.base === _d?.base);
const xp = ex?.denom_units.find((d) => d.denom === ex?.display)?.exponent ?? 0;
const amount = await this.convertToBaseUnit(Number(parameters.amount.amount), xp);

const hash = await walletClient.sendTransaction({
message: {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: {
fromAddress: walletClient.getAddress(),
toAddress: parameters.toAddress,
amount: [{ denom: _d?.base, amount: amount.toString() }],
},
},
});

if (!hash.value.transactionHash) throw new Error("transaction was incomplete");

return hash.value.transactionHash;
} catch (error) {
throw Error(`Failed to send token: ${error}`);
}
}

private async getChainInfo(walletClient: CosmosClient) {
const id = await walletClient.getChainId();
const chain = chains.find((ch) => ch.chain_id === id);

if (!chain) throw new Error("Network data is unavailable");

const asset = assets.find((ast) => ast.chain_name === chain?.chain_name);
return {
chain: chain,
asset: asset,
};
}

private async convertToBaseUnit(amount: number, decimals: number) {
const baseUnit = amount * 10 ** decimals;
return Number(baseUnit);
}

private async convertFromBaseUnit(amount: number, decimals: number) {
const decimalUnit = amount / 10 ** decimals;
return Number(decimalUnit);
}
}
1 change: 1 addition & 0 deletions typescript/packages/plugins/cosmosbank/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./bank.plugin";
Loading

0 comments on commit a6970f8

Please sign in to comment.