-
Notifications
You must be signed in to change notification settings - Fork 526
feat: added Noves Intents action provider #762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@coinbase/agentkit": minor | ||
--- | ||
|
||
Added a new action provider to interact with Noves Intents |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
|
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). |
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor -> patch