Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
7 changes: 5 additions & 2 deletions cdp-agentkit-core/typescript/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,9 @@
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist"],
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"lint": "npx --yes eslint -c .eslintrc.json src/**/*.ts",
Expand DownExpand Up@@ -38,8 +40,9 @@
],
"dependencies": {
"@coinbase/coinbase-sdk": "^0.15.0",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.21.51",
"viem": "^2.22.16",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
139 changes: 139 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/action_decorator.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";

import "reflect-metadata";

/**
* Parameters for the create action decorator
*/
export interface CreateActionDecoratorParams {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;
}

/**
* Metadata key for the action decorator
*/
export const ACTION_DECORATOR_KEY = Symbol("agentkit:action");

/**
* Metadata for AgentKit actions
*/
export interface ActionMetadata {
/**
* The name of the action
*/
name: string;

/**
* The description of the action
*/
description: string;

/**
* The schema of the action
*/
schema: z.ZodSchema;

/**
* The function to invoke the action
*/
invoke: Function;

/**
* The wallet provider to use for the action
*/
walletProvider: boolean;
}

/**
* A map of action names to their metadata
*/
export type StoredActionMetadata = Map<string, ActionMetadata>;

/**
* Decorator to embed metadata on class methods to indicate they are actions accessible to the agent
*
* @param params - The parameters for the action decorator
* @returns A decorator function
*
* @example
* ```typescript
* class MyActionProvider extends ActionProvider {
* @CreateAction({ name: "my_action", description: "My action", schema: myActionSchema })
* public myAction(args: z.infer<typeof myActionSchema>) {
* // ...
* }
* }
* ```
*/
export function CreateAction(params: CreateActionDecoratorParams) {
return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
const existingMetadata: StoredActionMetadata =
Reflect.getMetadata(ACTION_DECORATOR_KEY, target.constructor) || new Map();

const { isWalletProvider } = validateActionMethodArguments(target, propertyKey);

const metaData: ActionMetadata = {
name: params.name,
description: params.description,
schema: params.schema,
invoke: descriptor.value,
walletProvider: isWalletProvider,
};

existingMetadata.set(propertyKey, metaData);

Reflect.defineMetadata(ACTION_DECORATOR_KEY, existingMetadata, target.constructor);

return target;
};
}

function validateActionMethodArguments(
target: Object,
propertyKey: string,
): {
isWalletProvider: boolean;
} {
const className = target instanceof Object ? target.constructor.name : undefined;

const params = Reflect.getMetadata("design:paramtypes", target, propertyKey);

if (params == null) {
throw new Error(
`Failed to get parameters for action method ${propertyKey} on class ${className}`,
);
}

if (params.length > 2) {
throw new Error(
`Action method ${propertyKey} on class ${className} has more than 2 parameters`,
);
}

const walletProviderParam = params.find(param => {
if (!param || !param.prototype) {
return false;
}

if (param === WalletProvider) return true;
return param.prototype instanceof WalletProvider;
});

return {
isWalletProvider: !!walletProviderParam,
};
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
import { z } from "zod";
import { WalletProvider } from "../wallet_providers/wallet_provider";
import { Network } from "../wallet_providers/wallet_provider";
import { StoredActionMetadata, ACTION_DECORATOR_KEY } from "./action_decorator";

/**
* Action is the interface for all actions.
*/
export interface Action<TActionSchema extends z.ZodSchema = z.ZodSchema> {
name: string;
description: string;
schema: TActionSchema;
invoke: (args: z.infer<TActionSchema>) => Promise<string>;
}

/**
* ActionProvider is the abstract base class for all action providers.
*
* @abstract
*/
export abstract class ActionProvider<TWalletProvider extends WalletProvider = WalletProvider> {
/**
* The name of the action provider.
*/
public readonly name: string;

/**
* The action providers to combine.
*/
public readonly actionProviders: ActionProvider<TWalletProvider>[];

/**
* The constructor for the action provider.
*
* @param name - The name of the action provider.
* @param actionProviders - The action providers to combine.
*/
constructor(name: string, actionProviders: ActionProvider<TWalletProvider>[]) {
this.name = name;
this.actionProviders = actionProviders;
}

/**
* Checks if the action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the action provider supports the network, false otherwise.
*/
abstract supportsNetwork(network: Network): boolean;

/**
* Gets the actions of the action provider bound to the given wallet provider.
*
* @param walletProvider - The wallet provider.
* @returns The actions of the action provider.
*/
getActions(walletProvider: TWalletProvider): Action[] {
const actions: Action[] = [];

const actionProviders = [this, ...this.actionProviders];

for (const actionProvider of actionProviders) {
const actionsMetadataMap: StoredActionMetadata | undefined = Reflect.getMetadata(
ACTION_DECORATOR_KEY,
actionProvider.constructor,
);

if (!actionsMetadataMap) {
if (!(actionProvider instanceof ActionProvider)) {
console.warn(`Warning: ${actionProvider} is not an instance of ActionProvider.`);
} else {
console.warn(`Warning: ${actionProvider} has no actions.`);
}

continue;
}

for (const actionMetadata of actionsMetadataMap.values()) {
actions.push({
name: actionMetadata.name,
description: actionMetadata.description,
schema: actionMetadata.schema,
invoke: schemaArgs => {
const args: any[] = [];
if (actionMetadata.walletProvider) {
args[0] = walletProvider;
}

args.push(schemaArgs);

return actionMetadata.invoke.apply(actionProvider, args);
},
});
}
}

return actions;
}
}
4 changes: 4 additions & 0 deletions cdp-agentkit-core/typescript/src/action_providers/index.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
export * from "./action_decorator";
export * from "./action_provider";

export * from "./pyth";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
# Pyth Action Provider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
export * from "./pyth_action_provider";
export * from "./schemas";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { ActionProvider } from "../action_provider";
import { Network } from "../../wallet_providers/wallet_provider";
import { CreateAction } from "../action_decorator";
import { PythFetchPriceFeedIDSchema, PythFetchPriceSchema } from "./schemas";

/**
* PythActionProvider is an action provider for Pyth.
*/
export class PythActionProvider extends ActionProvider {
constructor() {
super("pyth", []);
}

/**
* Checks if the Pyth action provider supports the given network.
*
* @param network - The network to check.
* @returns True if the Pyth action provider supports the network, false otherwise.
*/
supportsNetwork = (_: Network) => true;

/**
* Fetch the price feed ID for a given token symbol from Pyth.
*
* @param args - The arguments for the action.
* @returns The price feed ID as a string.
*/
@CreateAction({
name: "fetch_price_feed",
description: "Fetch the price feed ID for a given token symbol from Pyth.",
schema: PythFetchPriceFeedIDSchema,
})
async fetchPriceFeed(args: z.infer<typeof PythFetchPriceFeedIDSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/price_feeds?query=${args.tokenSymbol}&asset_type=crypto`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();

if (data.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

const filteredData = data.filter(
(item: any) => item.attributes.base.toLowerCase() === args.tokenSymbol.toLowerCase(),
);

if (filteredData.length === 0) {
throw new Error(`No price feed found for ${args.tokenSymbol}`);
}

return filteredData[0].id;
}

/**
* Fetches the price from Pyth given a Pyth price feed ID.
*
* @param args - The arguments for the action.
* @returns The price as a string.
*/
@CreateAction({
name: "fetch_price",
description: `Fetch the price of a given price feed from Pyth.

Inputs:
- Pyth price feed ID

Important notes:
- Do not assume that a random ID is a Pyth price feed ID. If you are confused, ask a clarifying question.
- This action only fetches price inputs from Pyth price feeds. No other source.
- If you are asked to fetch the price from Pyth for a ticker symbol such as BTC, you must first use the pyth_fetch_price_feed_id
action to retrieve the price feed ID before invoking the pyth_Fetch_price action
`,
schema: PythFetchPriceSchema
})
async fetchPrice(args: z.infer<typeof PythFetchPriceSchema>): Promise<string> {
const url = `https://hermes.pyth.network/v2/updates/price/latest?ids[]=${args.priceFeedID}`;
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
const parsedData = data.parsed;

if (parsedData.length === 0) {
throw new Error(`No price data found for ${args.priceFeedID}`);
}

const priceInfo = parsedData[0].price;
const price = BigInt(priceInfo.price);
const exponent = priceInfo.expo;

if (exponent < 0) {
const adjustedPrice = price * BigInt(100);
const divisor = BigInt(10) ** BigInt(-exponent);
const scaledPrice = adjustedPrice / BigInt(divisor);
const priceStr = scaledPrice.toString();
const formattedPrice = `${priceStr.slice(0, -2)}.${priceStr.slice(-2)}`;
return formattedPrice.startsWith(".") ? `0${formattedPrice}` : formattedPrice;
}

const scaledPrice = price / BigInt(10) ** BigInt(exponent);
return scaledPrice.toString();
}
}

export const pythActionProvider = () => new PythActionProvider();
Loading