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
5 changes: 5 additions & 0 deletions typescript/.changeset/big-ads-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/agentkit": minor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor -> patch

---

Added a new action provider to interact with Noves Intents
1 change: 1 addition & 0 deletions typescript/agentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@coinbase/cdp-sdk": "^1.3.0",
"@coinbase/coinbase-sdk": "^0.20.0",
"@jup-ag/api": "^6.0.39",
"@noves/intent-ethers-provider": "^0.1.3",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason why this doesn't use noves-sdk instead? ethers dependency appears unnecessary here

"@privy-io/public-api": "2.18.5",
"@privy-io/server-auth": "1.18.4",
"@solana/spl-token": "^0.4.12",
Expand Down
40 changes: 40 additions & 0 deletions typescript/agentkit/src/action-providers/noves/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Noves Action Provider

This directory contains the **NovesActionProvider** implementation, which provides actions to interact with **Noves Intents** for retrieving token prices, transaction descriptions, and recent transactions.

## Directory Structure

```
noves/
├── novesActionProvider.ts # Main provider with Noves Intents functionality
├── novesActionProvider.test.ts # Test file for Noves provider
├── schemas.ts # Noves action schemas
├── index.ts # Main exports
└── README.md # This file
```

## Actions

- `getTranslatedTransaction`: Get a human-readable description of a transaction for specified transaction hash and network

- `getRecentTransactions`: Get a list of recent transactions on a given chain for a given wallet, with a human-readable description

- `getTokenCurrentPrice`: Get the price of a token at the given timestamp, or the current price if no timestamp is provided

## Adding New Actions

To add new Alchemy actions:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alchemy -> Noves


1. Define your action schema in `schemas.ts`
2. Implement the action in `novesActionProvider.ts`
3. Add tests in `novesActionProvider.test.ts`

## Network Support

Noves Intents support 100+ blockchain networks. For more information, visit [Noves](https://www.noves.fi/).

## Notes

- Rate limits applied

For more information on the **Noves API**, visit [Noves API Documentation](https://docs.noves.fi/reference/api-overview).
2 changes: 2 additions & 0 deletions typescript/agentkit/src/action-providers/noves/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./novesActionProvider";
export * from "./schemas";
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { NovesActionProvider } from "./novesActionProvider";

const mockGetRecentTxs = jest.fn();
const mockGetTranslatedTx = jest.fn();
const mockGetTokenPrice = jest.fn();

jest.mock("@noves/intent-ethers-provider", () => {
return {
IntentProvider: jest.fn().mockImplementation(() => ({
getRecentTxs: mockGetRecentTxs,
getTranslatedTx: mockGetTranslatedTx,
getTokenPrice: mockGetTokenPrice,
})),
};
});

describe("NovesActionProvider", () => {
let actionProvider: NovesActionProvider;

/**
* Set up test environment before each test.
* Initializes mocks and creates fresh instances of required objects.
*/
beforeEach(() => {
jest.clearAllMocks();
actionProvider = new NovesActionProvider();
});

describe("supportsNetwork", () => {
/**
* Test that the provider correctly supports all networks
*/
it("should return true for all networks", () => {
expect(actionProvider.supportsNetwork()).toBe(true);
});
});

describe("getRecentTransactions", () => {
const recentTxsArgs = {
chain: "base",
wallet: "0x2748f93c54042bfbe8691cdf3bd19adb12f7cfa0e30ea38128c5ed606b7a7f44",
};

const mockRecentTxs = [
{
txTypeVersion: 2,
chain: "base",
accountAddress: "0x2748f93c54042bfbe8691cdf3bd19adb12f7cfa0e30ea38128c5ed606b7a7f44",
classificationData: {
type: "transfer",
source: { type: "inference" },
description: "Transferred 1 ETH",
protocol: { name: null },
sent: [],
received: [],
},
rawTransactionData: {
transactionHash: "0x1234567890abcdef",
fromAddress: "0x2748f93c54042bfbe8691cdf3bd19adb12f7cfa0e30ea38128c5ed606b7a7f44",
toAddress: "0x9876543210abcdef",
blockNumber: 26586523,
gas: 5000000,
gasUsed: 377942,
gasPrice: 1826005,
transactionFee: {
amount: "0.000000693359358418",
token: { symbol: "ETH", name: "ETH", decimals: 18, address: "ETH" },
},
timestamp: 1739962393,
},
},
];

it("should get recent transactions successfully", async () => {
mockGetRecentTxs.mockResolvedValue(mockRecentTxs);
const result = await actionProvider.getRecentTransactions(recentTxsArgs);

expect(mockGetRecentTxs).toHaveBeenCalledWith(recentTxsArgs.chain, recentTxsArgs.wallet);
expect(result).toEqual(JSON.stringify(mockRecentTxs, null, 2));
});

it("should handle errors when getting recent transactions", async () => {
const error = new Error("Failed to fetch recent transactions");
mockGetRecentTxs.mockRejectedValue(error);

const result = await actionProvider.getRecentTransactions(recentTxsArgs);
expect(result).toEqual(`Error getting recent transactions: ${error}`);
});
});

describe("getTokenCurrentPrice", () => {
const tokenPriceArgs = {
chain: "base",
token_address: "0x1234567890abcdef",
};

const mockTokenPrice = {
price: "1800.50",
token: {
symbol: "ETH",
name: "Ethereum",
decimals: 18,
address: "0x1234567890abcdef",
},
timestamp: 1739962393,
};

it("should get token price successfully", async () => {
mockGetTokenPrice.mockResolvedValue(mockTokenPrice);
const result = await actionProvider.getTokenCurrentPrice(tokenPriceArgs);

expect(mockGetTokenPrice).toHaveBeenCalledWith({
chain: tokenPriceArgs.chain,
token_address: tokenPriceArgs.token_address,
});
expect(result).toEqual(JSON.stringify(mockTokenPrice, null, 2));
});

it("should handle errors when getting token price", async () => {
const error = new Error("Failed to fetch token price");
mockGetTokenPrice.mockRejectedValue(error);

const result = await actionProvider.getTokenCurrentPrice(tokenPriceArgs);
expect(result).toEqual(`Error getting token price: ${error}`);
});
});

describe("getTranslatedTransaction", () => {
const translatedTxArgs = {
chain: "base",
tx: "0x2748f93c54042bfbe8691cdf3bd19adb12f7cfa0e30ea38128c5ed606b7a7f44",
};

const mockTranslatedTx = {
txTypeVersion: 2,
chain: "base",
accountAddress: "0xed4B16e8c43AD2f6e08e6E9Bf1b8848644A8C6F6",
classificationData: {
type: "stakeNFT",
source: { type: "inference" },
description: "Staked Slipstream Position NFT v1 #7595063.",
protocol: { name: null },
sent: [
{
action: "staked",
amount: "1",
nft: {
name: "Slipstream Position NFT v1",
id: "7595063",
symbol: "AERO-CL-POS",
address: "0x827922686190790b37229fd06084350E74485b72",
},
from: { name: "This wallet", address: "0xed4B16e8c43AD2f6e08e6E9Bf1b8848644A8C6F6" },
to: { name: null, address: "0x282ece21a112950f62EC4493E2Bd2b27a74c7937" },
},
{
action: "paidGas",
from: { name: "This wallet", address: "0xed4B16e8c43AD2f6e08e6E9Bf1b8848644A8C6F6" },
to: { name: null, address: null },
amount: "0.000000693359358418",
token: { symbol: "ETH", name: "ETH", decimals: 18, address: "ETH" },
},
],
received: [],
},
rawTransactionData: {
transactionHash: "0x2748f93c54042bfbe8691cdf3bd19adb12f7cfa0e30ea38128c5ed606b7a7f44",
fromAddress: "0xed4B16e8c43AD2f6e08e6E9Bf1b8848644A8C6F6",
toAddress: "0x282ece21a112950f62EC4493E2Bd2b27a74c7937",
blockNumber: 26586523,
gas: 5000000,
gasUsed: 377942,
gasPrice: 1826005,
l1Gas: 1600,
l1GasPrice: 726849409,
transactionFee: {
amount: "0.000000693359358418",
token: { symbol: "ETH", name: "ETH", decimals: 18, address: "ETH" },
},
timestamp: 1739962393,
},
};

it("should get translated transaction successfully", async () => {
mockGetTranslatedTx.mockResolvedValue(mockTranslatedTx);
const result = await actionProvider.getTranslatedTransaction(translatedTxArgs);

expect(mockGetTranslatedTx).toHaveBeenCalledWith(translatedTxArgs.chain, translatedTxArgs.tx);
expect(result).toEqual(JSON.stringify(mockTranslatedTx, null, 2));
});

it("should handle errors when getting translated transaction", async () => {
const error = new Error("Failed to fetch translated transaction");
mockGetTranslatedTx.mockRejectedValue(error);

const result = await actionProvider.getTranslatedTransaction(translatedTxArgs);
expect(result).toEqual(`Error getting translated transaction: ${error}`);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { z } from "zod";
import { ActionProvider } from "../actionProvider";
import { CreateAction } from "../actionDecorator";
import {
NovesTranslatedTxSchema,
NovesRecentTxsSchema,
NovesTokenCurrentPriceSchema,
} from "./schemas";
import { IntentProvider } from "@noves/intent-ethers-provider";

/**
* NovesActionProvider is an action provider for fetching transaction descriptions, recent transactions, and token prices via Noves Intents.
*/
export class NovesActionProvider extends ActionProvider {
private intentProvider: IntentProvider;

/**
* Creates a new instance of NovesActionProvider
*/
constructor() {
super("noves", []);
this.intentProvider = new IntentProvider();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this not require an API key?

}

/**
* Get a human-readable description of a transaction
*
* @param args - The arguments containing the transaction hash and the chain.
* @returns A JSON string with the transaction description or an error message.
*/
@CreateAction({
name: "getTranslatedTransaction",
description:
"This tool will fetch a human-readable description of a transaction on a given chain",
schema: NovesTranslatedTxSchema,
})
async getTranslatedTransaction(args: z.infer<typeof NovesTranslatedTxSchema>): Promise<string> {
try {
const tx = await this.intentProvider.getTranslatedTx(args.chain, args.tx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think would be better to default to the walletProvider.getNetwork here. Might need a untility function to convert CDP network format to Noves format, eg base-mainnet -> base

return JSON.stringify(tx, null, 2);
} catch (error) {
return `Error getting translated transaction: ${error}`;
}
}

/**
* Get a list of recent transactions
*
* @param args - The arguments containing the chain and wallet address.
* @returns A JSON string with the list of recent transactions or an error message.
*/
@CreateAction({
name: "getRecentTransactions",
description:
"This tool will fetch a list of recent transactions for a given wallet on a given chain",
schema: NovesRecentTxsSchema,
})
async getRecentTransactions(args: z.infer<typeof NovesRecentTxsSchema>): Promise<string> {
try {
const txs = await this.intentProvider.getRecentTxs(args.chain, args.wallet);
return JSON.stringify(txs, null, 2);
} catch (error) {
return `Error getting recent transactions: ${error}`;
}
}

/**
* Get the current price of a token
*
* @param args - The arguments containing the chain, token address, and timestamp (optional).
* @returns A JSON string with the current token price or an error message.
*/
@CreateAction({
name: "getTokenCurrentPrice",
description:
"This tool will fetch the price of a token on a given chain at a given timestamp or the current price if no timestamp is provided",
schema: NovesTokenCurrentPriceSchema,
})
async getTokenCurrentPrice(args: z.infer<typeof NovesTokenCurrentPriceSchema>): Promise<string> {
try {
const price = await this.intentProvider.getTokenPrice({
chain: args.chain,
token_address: args.token_address,
timestamp: args.timestamp ? new Date(args.timestamp).getTime().toString() : undefined,
});
return JSON.stringify(price, null, 2);
} catch (error) {
return `Error getting token price: ${error}`;
}
}

/**
* Checks if the Noves action provider supports the given network.
* Since the API works with +100 networks, this always returns true.
*
* @returns Always returns true.
*/
supportsNetwork = (): boolean => {
return true;
};
}

/**
* Factory function to create a new NovesActionProvider instance.
*
* @returns A new instance of NovesActionProvider.
*/
export const novesActionProvider = () => new NovesActionProvider();
Loading
Loading