Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add action to list nfts on opensea (ts) #261

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions typescript/agentkit/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Added

- Added `openseaActionProvider` to list NFTs on OpenSea.
- Added `alchemyTokenPricesActionProvider` to fetch token prices from Alchemy.
- Added `token_prices_by_symbol` action to fetch token prices by symbol.
- Added `token_prices_by_address` action to fetch token prices by network and address pairs.
Expand All @@ -14,6 +15,7 @@

### Fixed

- Fixed mint function definition in ERC721_ABI.
- Added account argument in call to estimateGas in CdpWalletProvider
- Added explicit template type arguments for `ActionProvider` extensions

Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dependencies": {
"@coinbase/coinbase-sdk": "^0.17.0",
"md5": "^2.3.0",
"opensea-js": "^7.1.15",
"reflect-metadata": "^0.2.2",
"twitter-api-v2": "^1.18.2",
"viem": "^2.22.16",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
export const ERC721_ABI = [
{
inputs: [
{ internalType: "address", name: "to", type: "address" },
{ internalType: "uint256", name: "tokenId", type: "uint256" },
],
inputs: [{ internalType: "address", name: "to", type: "address" }],
name: "mint",
outputs: [],
payable: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("ERC721 Action Provider", () => {
data: encodeFunctionData({
abi: ERC721_ABI,
functionName: "mint",
args: [MOCK_DESTINATION, 1n],
args: [MOCK_DESTINATION],
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Do not use the contract address as the destination address. If you are unsure of
const data = encodeFunctionData({
abi: ERC721_ABI,
functionName: "mint",
args: [args.destination as Hex, 1n],
args: [args.destination as Hex],
});

const hash = await walletProvider.sendTransaction({
Expand Down
1 change: 1 addition & 0 deletions typescript/agentkit/src/action-providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from "./wallet";
export * from "./customActionProvider";
export * from "./alchemy";
export * from "./moonwell";
export * from "./opensea";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Opensea Action Provider
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./openseaActionProvider";
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { OpenSeaSDK } from "opensea-js";
import { openseaActionProvider } from "./openseaActionProvider";
import { Network } from "../../network";

jest.mock("opensea-js");

describe("OpenSea Action Provider", () => {
const MOCK_API_KEY = "test-api-key";
const MOCK_PRIVATE_KEY = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
const MOCK_CONTRACT = "0x1234567890123456789012345678901234567890";
const MOCK_TOKEN_ID = "1";
const MOCK_PRICE = 0.1;
const MOCK_EXPIRATION_DAYS = 90;
const MOCK_OPENSEA_BASE_URL = "https://testnets.opensea.io";
const MOCK_OPENSEA_CHAIN = "base_sepolia";

let actionProvider: ReturnType<typeof openseaActionProvider>;

beforeEach(() => {
jest.clearAllMocks();
// Mock OpenSeaSDK constructor
(OpenSeaSDK as jest.Mock).mockImplementation(() => ({
createListing: jest.fn(),
api: {
apiBaseUrl: MOCK_OPENSEA_BASE_URL,
},
chain: MOCK_OPENSEA_CHAIN,
}));
actionProvider = openseaActionProvider({
apiKey: MOCK_API_KEY,
privateKey: MOCK_PRIVATE_KEY,
networkId: "base-sepolia",
});
});

describe("listNft", () => {
it("should successfully list an NFT", async () => {
const mockListing = {};
const mockCreateListing = jest.fn().mockResolvedValue(mockListing);

// Update the mock implementation with the mock function
(OpenSeaSDK as jest.Mock).mockImplementation(() => ({
createListing: mockCreateListing,
api: {
apiBaseUrl: MOCK_OPENSEA_BASE_URL,
},
chain: MOCK_OPENSEA_CHAIN,
}));

// Re-create provider with new mock
actionProvider = openseaActionProvider({
apiKey: MOCK_API_KEY,
privateKey: MOCK_PRIVATE_KEY,
networkId: "base-sepolia",
});

const args = {
contractAddress: MOCK_CONTRACT,
tokenId: MOCK_TOKEN_ID,
price: MOCK_PRICE,
expirationDays: MOCK_EXPIRATION_DAYS,
};

const response = await actionProvider.listNft(args);

expect(mockCreateListing).toHaveBeenCalledWith(
expect.objectContaining({
asset: {
tokenId: MOCK_TOKEN_ID,
tokenAddress: MOCK_CONTRACT,
},
startAmount: MOCK_PRICE,
quantity: 1,
}),
);

expect(response).toBe(
`Successfully listed NFT ${MOCK_CONTRACT} token ${MOCK_TOKEN_ID} for ${MOCK_PRICE} ETH, expiring in ${MOCK_EXPIRATION_DAYS} days. Listing on OpenSea: ${MOCK_OPENSEA_BASE_URL}/assets/${MOCK_OPENSEA_CHAIN}/${MOCK_CONTRACT}/${MOCK_TOKEN_ID}.`,
);
});

it("should handle listing errors", async () => {
const error = new Error("Listing failed");
const mockCreateListing = jest.fn().mockRejectedValue(error);

// Update the mock implementation with the mock function
(OpenSeaSDK as jest.Mock).mockImplementation(() => ({
createListing: mockCreateListing,
api: {
apiBaseUrl: MOCK_OPENSEA_BASE_URL,
},
chain: MOCK_OPENSEA_CHAIN,
}));

// Re-create provider with new mock
actionProvider = openseaActionProvider({
apiKey: MOCK_API_KEY,
privateKey: MOCK_PRIVATE_KEY,
networkId: "base-sepolia",
});

const args = {
contractAddress: MOCK_CONTRACT,
tokenId: MOCK_TOKEN_ID,
price: MOCK_PRICE,
expirationDays: MOCK_EXPIRATION_DAYS,
};

const response = await actionProvider.listNft(args);
expect(response).toContain(`Error listing NFT ${MOCK_CONTRACT} token ${MOCK_TOKEN_ID}`);
expect(response).toContain(error.message);
});
});

describe("supportsNetwork", () => {
it("should return true for supported networks", () => {
const baseSepoliaNetwork: Network = {
protocolFamily: "evm",
networkId: "base-sepolia",
chainId: "84532",
};
expect(actionProvider.supportsNetwork(baseSepoliaNetwork)).toBe(true);
});

it("should return false for unsupported networks", () => {
const fantomNetwork: Network = {
protocolFamily: "bitcoin",
networkId: "any",
};
expect(actionProvider.supportsNetwork(fantomNetwork)).toBe(false);
});
});
});
Loading