UNPKG

@dojima-wallet/solver-modules

Version:

Modules of different protocols for solvers

1,033 lines (957 loc) 28.3 kB
import { OpenSeaSDK, Chain, EventType, EventData, OrderV2, CollectionOffer, AssetWithTokenId, Order, AssetWithTokenStandard, } from "opensea-js"; import { BigNumberish, ethers, Overrides } from "ethers"; // Import BigNumberish from seaport-js to match OpenSea's types // import { BigNumberish } from '@opensea/seaport-js/node_modules/ethers/lib/utils'; export class OpenSeaClient { private sdk: OpenSeaSDK; private provider: ethers.providers.JsonRpcProvider; private signer: ethers.Wallet; private chain: string; private rpcUrl: string; protected openseaApiKey: string; constructor(privateKey: string, chain: string, openseaApiKey: string) { this.chain = chain; this.rpcUrl = this.getRpcUrl(); this.provider = new ethers.providers.JsonRpcProvider(this.rpcUrl); this.openseaApiKey = openseaApiKey; // Create a signer with the required signTypedData method const signer = new ethers.Wallet(privateKey, this.provider); // Add signTypedData method to the signer (signer as any).signTypedData = async ( domain: any, types: any, value: any ) => { return signer._signTypedData(domain, types, value); }; this.signer = signer; // Initialize OpenSea SDK with the enhanced signer this.sdk = new OpenSeaSDK(this.signer as any, { chain: this.getChain(), apiKey: this.openseaApiKey, }); } // Add a method to get WETH contract async getWethContract() { const wethAddress = "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14"; // Sepolia WETH address return new ethers.Contract( wethAddress, [ "function deposit() payable", "function withdraw(uint256 amount)", "function balanceOf(address owner) view returns (uint256)", "function approve(address spender, uint256 amount) returns (bool)", ], this.signer ); } getChain(): Chain { switch (this.chain.toLowerCase()) { case "mainnet": return Chain.Mainnet; case "sepolia": return Chain.Sepolia; case "base_sepolia": return Chain.BaseSepolia; default: throw new Error(`Unsupported chain: ${this.chain}`); } } getRpcUrl(): string { switch (this.chain.toLowerCase()) { case "mainnet": return "https://eth-mainnet.public.blastapi.io"; case "sepolia": return "https://ethereum-sepolia-rpc.publicnode.com"; case "goerli": return "https://ethereum-goerli-rpc.publicnode.com"; default: throw new Error(`RPC URL not configured for chain: ${this.chain}`); } } /** * Add a listener for events emitted by the SDK. * @param event The EventType to listen to * @param listener A callback that will accept an object with EventData * @param once Whether the listener should only be called once, or continue listening until removed */ public addListener( event: EventType, listener: (data: EventData) => void, once = false ): void { this.sdk.addListener(event, listener, once); } /** * Instead of signing an off-chain order, this methods allows you to approve an order * with on on-chain transaction. * @param order Order to approve * @param domain An optional domain to be hashed and included at the end of fulfillment calldata * @returns Transaction hash of the approval transaction */ public async approveOrder(order: OrderV2, domain?: string): Promise<string> { try { return await this.sdk.approveOrder(order, domain); } catch (error) { this.handleError(error, "approveOrder"); throw error; } } /** * Cancel an order onchain, preventing it from ever being fulfilled. * @param options.order The order to cancel * @param options.accountAddress The account address that will be cancelling the order * @param options.domain An optional domain to be hashed and included at the end of fulfillment calldata */ public async cancelOrder({ order, accountAddress, domain, }: { order: OrderV2; accountAddress: string; domain?: string; }): Promise<void> { try { await this.sdk.cancelOrder({ order, accountAddress, domain, }); } catch (error) { this.handleError(error, "cancelOrder"); throw error; } } /** * Create and submit a collection offer. * @param options Parameters for creating the collection offer * @returns The CollectionOffer that was created */ public async createCollectionOffer({ collectionSlug, accountAddress, amount, quantity, domain, salt, expirationTime, paymentTokenAddress, excludeOptionalCreatorFees = false, offerProtectionEnabled = true, traitType, traitValue, }: { collectionSlug: string; accountAddress: string; amount: BigNumberish; quantity: number; domain?: string; salt?: BigNumberish; expirationTime?: number | string; paymentTokenAddress: string; excludeOptionalCreatorFees?: boolean; offerProtectionEnabled?: boolean; traitType?: string; traitValue?: string; }): Promise<CollectionOffer | null> { try { return await this.sdk.createCollectionOffer({ collectionSlug, accountAddress, amount: amount.toString(), quantity, domain, salt: salt?.toString(), expirationTime, paymentTokenAddress, excludeOptionalCreatorFees, offerProtectionEnabled, traitType, traitValue, }); } catch (error) { this.handleError(error, "createCollectionOffer"); throw error; } } /** * Create and submit a listing for an asset. * @param options Parameters for creating the listing * @returns The OrderV2 that was created */ public async createListing({ asset, accountAddress, startAmount, endAmount, quantity = 1, domain, salt, listingTime, expirationTime, paymentTokenAddress = ethers.constants.AddressZero, buyerAddress, englishAuction, excludeOptionalCreatorFees = false, zone = ethers.constants.AddressZero, }: { asset: AssetWithTokenId; accountAddress: string; startAmount: BigNumberish; endAmount?: BigNumberish; quantity?: BigNumberish; domain?: string; salt?: BigNumberish; listingTime?: number; expirationTime?: number; paymentTokenAddress?: string; buyerAddress?: string; englishAuction?: boolean; excludeOptionalCreatorFees?: boolean; zone?: string; }): Promise<OrderV2> { try { return await this.sdk.createListing({ asset, accountAddress, startAmount: startAmount.toString(), endAmount: endAmount?.toString(), quantity: quantity?.toString(), domain, salt: salt?.toString(), listingTime, expirationTime, paymentTokenAddress, buyerAddress, englishAuction, excludeOptionalCreatorFees, zone, }); } catch (error) { this.handleError(error, "createListing"); throw error; } } /** * Create and submit an offer on an asset. * @param options Parameters for creating the offer * @returns The OrderV2 that was created */ public async createOffer({ asset, accountAddress, startAmount, domain, salt, expirationTime, paymentTokenAddress = ethers.constants.AddressZero, quantity = 1, excludeOptionalCreatorFees = true, zone = ethers.constants.AddressZero, }: { asset: AssetWithTokenId; accountAddress: string; startAmount: BigNumberish; domain?: string; salt?: BigNumberish; expirationTime?: BigNumberish; paymentTokenAddress?: string; quantity?: BigNumberish; excludeOptionalCreatorFees?: boolean; zone?: string; }): Promise<OrderV2> { try { return await this.sdk.createOffer({ asset, accountAddress, startAmount: startAmount.toString(), domain, salt: salt?.toString(), expirationTime: expirationTime?.toString(), paymentTokenAddress, quantity: quantity?.toString(), excludeOptionalCreatorFees, zone, }); } catch (error) { this.handleError(error, "createOffer"); throw error; } } /** * Fulfill an order for an asset. The order can be either a listing or an offer. * @param options Parameters for fulfilling the order * @returns Transaction hash of the order */ public async fulfillOrder({ order, accountAddress, domain, recipientAddress, overrides, }: { order: OrderV2 | Order; accountAddress: string; domain?: string; recipientAddress?: string; overrides?: Overrides; }): Promise<string> { try { return await this.sdk.fulfillOrder({ order, accountAddress, domain, recipientAddress, // overrides, }); } catch (error) { this.handleError(error, "fulfillOrder"); throw error; } } /** * Get an account's balance of any Asset. This asset can be an ERC20, ERC1155, or ERC721. * @param options Parameters for getting the balance * @returns The balance of the asset for the account * @throws Error if the token standard does not support balanceOf */ public async getBalance({ accountAddress, asset, }: { accountAddress: string; asset: AssetWithTokenStandard; }): Promise<bigint> { try { return await this.sdk.getBalance({ accountAddress, asset, }); } catch (error) { this.handleError(error, "getBalance"); throw error; } } // Helper method to handle errors private handleError(error: any, context: string) { console.error(`Error in ${context}:`, error); throw error; } /** * Transfer an asset. This asset can be an ERC20, ERC1155, or ERC721. * @param options Parameters for transferring the asset * @returns Promise that resolves when the transfer is complete */ public async transfer({ amount, asset, fromAddress, overrides, toAddress, }: { amount?: BigNumberish; asset: AssetWithTokenStandard; fromAddress: string; overrides?: Overrides; toAddress: string; }): Promise<void> { try { await this.sdk.transfer({ amount: amount?.toString(), asset, fromAddress, toAddress, // overrides temporarily disabled due to type mismatch // overrides, }); } catch (error) { this.handleError(error, "transfer"); throw error; } } /** * Unwrap WETH into ETH. Emits the UnwrapWeth event when the transaction is prompted. * @param options Parameters for unwrapping WETH * @returns Promise that resolves when the unwrap is complete */ public async wrapEth({ accountAddress, amountInEth, }: { accountAddress: string; amountInEth: BigNumberish; }): Promise<void> { try { const wethContract = await this.getWethContract(); const tx = await wethContract.deposit({ value: amountInEth.toString(), gasLimit: 100000, }); await tx.wait(); } catch (error) { this.handleError(error, "wrapEth"); throw error; } } // problem in converting the value // public async wrapEth({ // accountAddress, // amountInEth, // }: { // accountAddress: string; // amountInEth: BigNumberish; // }): Promise<void> { // try { // console.log("amountInEth.toString(): ", amountInEth.toString()); // const amountInFormat = ethers.utils.formatEther(amountInEth.toString()); // console.log(`Amount in ETH: ${amountInFormat} ETH`); // await this.sdk.wrapEth({ // accountAddress, // amountInEth: amountInFormat.toString(), // }); // } catch (error) { // this.handleError(error, 'wrapEth'); // throw error; // } // } /** * Wrap ETH into WETH. W-ETH is needed for making offers. * @param options Parameters for wrapping ETH * @returns Promise that resolves when the wrap is complete */ public async unwrapWeth({ accountAddress, amountInEth, }: { accountAddress: string; amountInEth: BigNumberish; }): Promise<void> { try { const wethContract = await this.getWethContract(); const tx = await wethContract.withdraw(amountInEth.toString()); await tx.wait(); } catch (error) { this.handleError(error, "unwrapWeth"); throw error; } } // problem in converting the value // public async unwrapWeth({ // accountAddress, // amountInEth, // }: { // accountAddress: string; // amountInEth: BigNumberish; // }): Promise<void> { // try { // await this.sdk.unwrapWeth({ // accountAddress, // amountInEth: amountInEth.toString(), // }); // } catch (error) { // this.handleError(error, 'unwrapWeth'); // throw error; // } // } /** * Returns whether an order is fulfillable. * @param options Parameters for checking order fulfillability * @returns True if the order is fulfillable, else False * @throws Error if the order's protocol address is not supported by OpenSea */ public async isOrderFulfillable({ accountAddress, order, }: { accountAddress: string; order: OrderV2; }): Promise<boolean> { try { return await this.sdk.isOrderFulfillable({ accountAddress, order, }); } catch (error) { this.handleError(error, "isOrderFulfillable"); throw error; } } /** * Remove an event listener by calling .removeListener() on an event and listener. * @param event The EventType to remove a listener for * @param listener The listener to remove */ public removeListener( event: EventType, listener: (data: EventData) => void ): void { this.sdk.removeListener(event, listener); } /** * Remove all event listeners. * This should be called when you're unmounting a component that listens to events to make UI updates. * @param event Optional EventType to remove listeners for */ public removeAllListeners(event?: EventType): void { this.sdk.removeAllListeners(event); } /** * Get a list of OpenSea collections * @returns Promise with the collections data */ public async getCollections(): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/collections?chain=${this.chain}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getCollections"); throw error; } } /** * Get a single collection's details * @param collectionSlug The slug of the collection to fetch * @returns Promise with the collection details */ public async getCollection(collectionSlug: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/collections/${collectionSlug}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getCollection"); throw error; } } /** * Get an OpenSea Account Profile * @param addressOrUsername The address or username of the account to fetch * @returns Promise with the account profile data */ public async getAccount(addressOrUsername: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/accounts/${addressOrUsername}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getAccount"); throw error; } } /** * Get a smart contract for a given chain and address * @param chain The blockchain where the contract is deployed * @param address The contract address * @returns Promise with the contract details */ public async getContract(chain: string, address: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/chain/${chain}/contract/${address}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getContract"); throw error; } } /** * Get metadata, traits, ownership information, and rarity for a single NFT * @param chain The blockchain where the NFT exists * @param address The NFT contract address * @param identifier The token ID of the NFT * @returns Promise with the NFT details */ public async getNFT( chain: string, address: string, identifier: string ): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/chain/${chain}/contract/${address}/nfts/${identifier}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getNFT"); throw error; } } /** * Get NFTs owned by a given account address * @param chain The blockchain to query * @param address The account address to get NFTs for * @returns Promise with the account's NFTs */ public async getNFTsByAccount(chain: string, address: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/chain/${chain}/account/${address}/nfts`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getNFTsByAccount"); throw error; } } /** * Get multiple NFTs for a collection * @param collectionSlug The slug of the collection * @returns Promise with the collection's NFTs */ public async getNFTsByCollection(collectionSlug: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/collection/${collectionSlug}/nfts`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getNFTsByCollection"); throw error; } } /** * Get multiple NFTs for a smart contract * @param chain The blockchain where the contract exists * @param address The contract address * @returns Promise with the contract's NFTs */ public async getNFTsByContract(chain: string, address: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/chain/${chain}/contract/${address}/nfts`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getNFTsByContract"); throw error; } } /** * Refresh metadata for a single NFT * @param chain The blockchain where the NFT exists * @param address The NFT contract address * @param identifier The token ID of the NFT * @returns Promise with the refresh status */ public async refreshNFTMetadata( chain: string, address: string, identifier: string ): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/chain/${chain}/contract/${address}/nfts/${identifier}/refresh`, { method: "POST", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "refreshNFTMetadata"); throw error; } } /** * Get a list of events for an account * @param address The account address * @param options Optional parameters (after, before timestamps) * @returns Promise with the account's events */ public async getEventsByAccount( address: string, options?: { after?: number; before?: number } ): Promise<any> { try { let url = `https://testnets-api.opensea.io/api/v2/events/accounts/${address}`; // Add query parameters if provided if (options) { const params = new URLSearchParams(); if (options.after) params.append("after", options.after.toString()); if (options.before) params.append("before", options.before.toString()); if (params.toString()) url += `?${params.toString()}`; } const response = await fetch(url, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getEventsByAccount"); throw error; } } /** * Get a list of events for a collection * @param collectionSlug The collection slug * @returns Promise with the collection's events */ public async getEventsByCollection(collectionSlug: string): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/events/collection/${collectionSlug}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getEventsByCollection"); throw error; } } /** * Get a list of events for a single NFT * @param chain The blockchain where the NFT exists * @param address The NFT contract address * @param identifier The token ID of the NFT * @returns Promise with the NFT's events */ public async getEventsByNFT( chain: string, address: string, identifier: string ): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/events/chain/${chain}/contract/${address}/nfts/${identifier}`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getEventsByNFT"); throw error; } } /** * Buy a listed NFT on OpenSea * @param options Parameters for buying the NFT * @param options.order The listing order to fulfill * @param options.accountAddress Address of the wallet that will buy the NFT * @param options.recipientAddress Optional address to receive the NFT (defaults to accountAddress) * @returns Transaction hash of the purchase */ public async buyNFT( collectionSlug: string, identifier: string, accountAddress: string, recipientAddress?: string ): Promise<string> { try { // First get the best listing const response = await fetch( `https://testnets-api.opensea.io/api/v2/listings/collection/${collectionSlug}/nfts/${identifier}/best`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": process.env.OPENSEA_API_KEY || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const bestListing = await response.json(); if (!bestListing || !bestListing.protocol_data) { throw new Error("No valid listing found for this NFT"); } // Construct the order object in the format expected by the SDK const order = { protocol_address: bestListing.protocol_address, protocol_data: bestListing.protocol_data, order_hash: bestListing.order_hash, price: bestListing.price, chain: this.chain, }; // Then fulfill the order const txHash = await this.fulfillOrder({ order, accountAddress, recipientAddress: recipientAddress || accountAddress, }); // console.log(`✅ Successfully purchased NFT. Transaction hash: ${txHash}`); return txHash; } catch (error) { // console.error('❌ NFT purchase failed:', error); throw error; } } // public async buyNFT({ // order, // accountAddress, // recipientAddress, // }: { // order: OrderV2 | Order; // accountAddress: string; // recipientAddress?: string; // }): Promise<string> { // try { // // Fulfill the listing order // const txHash = await this.fulfillOrder({ // order, // accountAddress, // recipientAddress: recipientAddress || accountAddress, // }); // // console.log(`✅ Successfully purchased NFT. Transaction hash: ${txHash}`); // return txHash; // } catch (error) { // // console.error("❌ NFT purchase failed:", error); // throw error; // } // } /** * Get the best listing for an NFT * @param collectionSlug The collection slug * @param identifier The token ID of the NFT * @returns Promise with the best listing details */ public async getBestListingForNFT( collectionSlug: string, identifier: string ): Promise<any> { try { const response = await fetch( `https://testnets-api.opensea.io/api/v2/listings/collection/${collectionSlug}/nfts/${identifier}/best`, { method: "GET", headers: { Accept: "application/json", "X-API-KEY": this.openseaApiKey || "", }, } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { this.handleError(error, "getBestListingForNFT"); throw error; } } }