Skip to content

Commit

Permalink
remove network restriction
Browse files Browse the repository at this point in the history
  • Loading branch information
phdargen committed Feb 11, 2025
1 parent 78b2150 commit 3ae786a
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ describe("OpenSea Action Provider", () => {
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,
Expand All @@ -25,10 +35,24 @@ describe("OpenSea Action Provider", () => {

describe("listNft", () => {
it("should successfully list an NFT", async () => {
const mockListing = {
/* mock listing response */
};
(OpenSeaSDK.prototype.createListing as jest.Mock).mockResolvedValue(mockListing);
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,
Expand All @@ -39,7 +63,7 @@ describe("OpenSea Action Provider", () => {

const response = await actionProvider.listNft(args);

expect(OpenSeaSDK.prototype.createListing).toHaveBeenCalledWith(
expect(mockCreateListing).toHaveBeenCalledWith(
expect.objectContaining({
asset: {
tokenId: MOCK_TOKEN_ID,
Expand All @@ -51,13 +75,29 @@ describe("OpenSea Action Provider", () => {
);

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: https://testnets.opensea.io/assets/base_sepolia/${MOCK_CONTRACT}/${MOCK_TOKEN_ID}.`,
`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");
(OpenSeaSDK.prototype.createListing as jest.Mock).mockRejectedValue(error);
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,
Expand All @@ -79,23 +119,15 @@ describe("OpenSea Action Provider", () => {
networkId: "base-sepolia",
chainId: "84532",
};
const baseMainnetNetwork: Network = {
protocolFamily: "evm",
networkId: "base-mainnet",
chainId: "8453",
};

expect(actionProvider.supportsNetwork(baseSepoliaNetwork)).toBe(true);
expect(actionProvider.supportsNetwork(baseMainnetNetwork)).toBe(true);
});

it("should return false for unsupported networks", () => {
const ethereumNetwork: Network = {
protocolFamily: "evm",
networkId: "ethereum",
chainId: "1",
const fantomNetwork: Network = {
protocolFamily: "bitcoin",
networkId: "any",
};
expect(actionProvider.supportsNetwork(ethereumNetwork)).toBe(false);
expect(actionProvider.supportsNetwork(fantomNetwork)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { z } from "zod";
import { ActionProvider } from "../actionProvider";
import { CreateAction } from "../actionDecorator";
import { ListNftSchema } from "./schemas";
import { OpenSeaSDK, Chain } from "opensea-js";
import { Network } from "../../network";
import { Wallet, JsonRpcProvider } from "ethers";

import { OpenSeaSDK } from "opensea-js";
import { Network, NETWORK_ID_TO_CHAIN_ID } from "../../network";
import { Wallet, ethers } from "ethers";
import { EvmWalletProvider } from "../../wallet-providers";
import { chainIdToOpenseaChain, supportedChains } from "./utils";
/**
* Configuration options for the OpenseaActionProvider.
*/
Expand All @@ -26,23 +27,14 @@ export interface OpenseaActionProviderConfig {
privateKey?: string;
}

/**
* NetworkConfig is the configuration for network-specific settings.
*/
interface NetworkConfig {
rpcUrl: string;
openseaUrl: string;
chain: Chain;
}

/**
* OpenseaActionProvider is an action provider for OpenSea marketplace interactions.
*/
export class OpenseaActionProvider extends ActionProvider {
export class OpenseaActionProvider extends ActionProvider<EvmWalletProvider> {
private readonly apiKey: string;
private openseaSDK: OpenSeaSDK;
private walletWithProvider: Wallet;
private readonly networkConfig: NetworkConfig;
private openseaSDK: OpenSeaSDK;
private openseaBaseUrl: string;

/**
* Constructor for the OpenseaActionProvider class.
Expand All @@ -58,33 +50,17 @@ export class OpenseaActionProvider extends ActionProvider {
}
this.apiKey = apiKey;

this.networkConfig =
config.networkId === "base-sepolia"
? {
rpcUrl: "https://sepolia.base.org",
openseaUrl: "https://testnets.opensea.io/assets/base_sepolia",
chain: Chain.BaseSepolia,
}
: {
rpcUrl: "https://main.base.org",
openseaUrl: "https://opensea.io/assets/base",
chain: Chain.Mainnet,
};

if (config.networkId !== "base-sepolia" && config.networkId !== "base-mainnet") {
throw new Error("Unsupported network. Only base-sepolia and base-mainnet are supported.");
}

// Initialize ethers signer required for OpenSea SDK
const provider = new JsonRpcProvider(this.networkConfig.rpcUrl);
const chainId = NETWORK_ID_TO_CHAIN_ID[config.networkId || "base-sepolia"];
const provider = ethers.getDefaultProvider(parseInt(chainId));
const walletWithProvider = new Wallet(config.privateKey!, provider);
this.walletWithProvider = walletWithProvider;

const openseaSDK = new OpenSeaSDK(walletWithProvider, {
chain: this.networkConfig.chain,
chain: chainIdToOpenseaChain(chainId),
apiKey: this.apiKey,
});
this.openseaSDK = openseaSDK;
this.openseaBaseUrl = this.openseaSDK.api.apiBaseUrl.replace("-api", "").replace("api", "");
}

/**
Expand All @@ -97,7 +73,7 @@ export class OpenseaActionProvider extends ActionProvider {
name: "list_nft",
description: `
This tool will list an NFT for sale on the OpenSea marketplace.
Currently only base-sepolia and base-mainnet are supported.
EVM networks are supported on mainnet and testnets.
It takes the following inputs:
- contractAddress: The NFT contract address to list
Expand All @@ -110,10 +86,8 @@ Important notes:
- Price is in ETH (e.g., 1.5 for 1.5 ETH). This is the amount the seller will receive if the NFT is sold. It is not required to have this amount in the wallet.
- Listing the NFT requires approval for OpenSea to manage the entire NFT collection:
- If the collection is not already approved, an onchain transaction is required, which will incur gas fees.
- If already approved, listing is gasless and does not require any onchain transaction.
- Only the following networks are supported:
- Base Sepolia (base-sepolia)
- Base Mainnet (base, base-mainnet)
- If already approved, listing is gasless and does not require any onchain transaction.
- EVM networks are supported on mainnet and testnets, for example: base-mainnet and base-sepolia.
`,
schema: ListNftSchema,
})
Expand All @@ -127,13 +101,12 @@ Important notes:
},
startAmount: args.price,
quantity: 1,
paymentTokenAddress: "0x0000000000000000000000000000000000000000",
paymentTokenAddress: "0x0000000000000000000000000000000000000000", // ETH
expirationTime: expirationTime,
accountAddress: this.walletWithProvider.address,
});

const listingLink = `${this.networkConfig.openseaUrl}/${args.contractAddress}/${args.tokenId}`;

const listingLink = `${this.openseaBaseUrl}/assets/${this.openseaSDK.chain}/${args.contractAddress}/${args.tokenId}`;
return `Successfully listed NFT ${args.contractAddress} token ${args.tokenId} for ${args.price} ETH, expiring in ${args.expirationDays} days. Listing on OpenSea: ${listingLink}.`;
} catch (error) {
return `Error listing NFT ${args.contractAddress} token ${args.tokenId} for ${args.price} ETH using account ${this.walletWithProvider.address}: ${error}`;
Expand All @@ -146,8 +119,7 @@ Important notes:
* @param network - The network to check.
* @returns True if the Opensea action provider supports the network, false otherwise.
*/
supportsNetwork = (network: Network) =>
network.networkId === "base-mainnet" || network.networkId === "base-sepolia";
supportsNetwork = (network: Network) => supportedChains[network!.chainId!] !== undefined;
}

export const openseaActionProvider = (config?: OpenseaActionProviderConfig) =>
Expand Down
45 changes: 45 additions & 0 deletions typescript/agentkit/src/action-providers/opensea/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Chain } from "opensea-js";

/**
* Supported Opensea chains
*/
export const supportedChains: Record<string, Chain> = {
"1": Chain.Mainnet,
"137": Chain.Polygon,
"80002": Chain.Amoy,
"11155111": Chain.Sepolia,
"8217": Chain.Klaytn,
"1001": Chain.Baobab,
"43114": Chain.Avalanche,
"43113": Chain.Fuji,
"42161": Chain.Arbitrum,
"42170": Chain.ArbitrumNova,
"421614": Chain.ArbitrumSepolia,
"238": Chain.Blast,
"168587773": Chain.BlastSepolia,
"8453": Chain.Base,
"84532": Chain.BaseSepolia,
"10": Chain.Optimism,
"11155420": Chain.OptimismSepolia,
"7777777": Chain.Zora,
"999999999": Chain.ZoraSepolia,
"1329": Chain.Sei,
"1328": Chain.SeiTestnet,
"8333": Chain.B3,
"1993": Chain.B3Sepolia,
"80094": Chain.BeraChain,
};

/**
* Maps EVM chain IDs to Opensea chain
*
* @param chainId - The EVM chain ID to map
* @returns The corresponding OpenSea Chain enum value
*/
export const chainIdToOpenseaChain = (chainId: string): Chain => {
const chain = supportedChains[chainId];
if (!chain) {
throw new Error(`Unsupported chain ID on Opensea: ${chainId}`);
}
return chain;
};

0 comments on commit 3ae786a

Please sign in to comment.