UNPKG

hyperspace-sdk

Version:

An unofficial SDK for Hyperspace NFT Marketplace on Avalanche

1,375 lines (1,241 loc) 34 kB
import { Wallet } from "ethers"; import { JsonRpcBatchProvider } from "@ethersproject/providers"; import axios from "axios"; import { Result, Listing, CollectionBid, Buy, Sale, Order, Nft, OrderConfig, ProjectStats, } from "./types"; import { getWavaxBalance, getWavaxApprovalStatus, approveWavax, getNftApprovalStatus, approveNft, handleSignAndExecute, handleSignTransaction, } from "./utils/helpers"; import { PaginationConfig } from "./types"; export class Hyperspace { apiKey: string; wallet: Wallet; provider: JsonRpcBatchProvider; gasPriceMultiplier: number = 1; gasLimitMultiplier: number = 1; constructor(apiKey: string, rpcHttpUrl: string, privateKey: string) { this.apiKey = apiKey; this.provider = new JsonRpcBatchProvider(rpcHttpUrl); const wallet = new Wallet(`0x${privateKey}`); this.wallet = wallet.connect(this.provider); } getGasPriceMultiplier(): number { return this.gasPriceMultiplier; } updateGasPriceMultiplier( newGasPriceMultiplier: number ): Result<number, Error> { if (newGasPriceMultiplier < 1 || newGasPriceMultiplier > 20) { return { success: false, error: new Error("Gas price multiplier must be between 1 and 10"), }; } this.gasPriceMultiplier = newGasPriceMultiplier; return { success: true, value: newGasPriceMultiplier, }; } getGasLimitMultiplier(): number { return this.gasLimitMultiplier; } updateGasLimitMultiplier( newGasLimitMultiplier: number ): Result<number, Error> { if (newGasLimitMultiplier < 1 || newGasLimitMultiplier > 20) { return { success: false, error: new Error("Gas limit multiplier must be between 1 and 10"), }; } this.gasLimitMultiplier = newGasLimitMultiplier; return { success: true, value: newGasLimitMultiplier, }; } public async getListingsForProject( projectId?: string, paginationInfo?: PaginationConfig ): Promise<Result<Listing[], Error>> { const data = JSON.stringify({ condition: { project_ids: [ { project_id: projectId, }, ], listing_type: "NORMAL", }, order_by: { field_name: "listing_display_price", sort_order: "ASC", }, pagination_info: paginationInfo, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-collection-view", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error(`Unable to send request to fetch listings: ${error}`), }; } const listings: Listing[] = response.data.collection_view_data; if (!listings) { return { success: false, error: new Error("Unable to find listings"), }; } return { success: true, value: listings, }; } /** * Fetch listings for a specific collection. * Right now, it ONLY fetches in ASCENDING order of price. * @param projectId Project ID of the collection to fetch listings for. * @param quantity Maximum number of listings to fetch. * @returns Array of listings or error. */ // TODO: Is `quantity` really necessary? public async getListingsForUser( userAddress?: string, projectId?: string, paginationInfo?: PaginationConfig ): Promise<Result<Listing[], Error>> { const data = JSON.stringify({ condition: { owner: userAddress ? userAddress : this.wallet.address, project_ids: [ { project_id: projectId, }, ], listing_type: "NORMAL", }, order_by: { field_name: "listing_display_price", sort_order: "ASC", }, pagination_info: paginationInfo, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-collection-view", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error(`Unable to send request to fetch listings: ${error}`), }; } const listings: Listing[] = response.data.collection_view_data; if (!listings) { return { success: false, error: new Error("Unable to find listings"), }; } return { success: true, value: listings, }; } /** * Get a specific listing. * @param collectionAddress Address of the collection to fetch listing for. * @param tokenId Token ID of the listing to fetch. * @returns Listing or error. */ async getListing( collectionAddress: string, tokenId: string ): Promise<Result<Listing, Error>> { const data = JSON.stringify({ condition: { token_addresses: [`${collectionAddress}_${tokenId}`], }, order_by: { field_name: "listing_display_price", sort_order: "ASC", }, pagination_info: { page_number: 1, page_size: 1, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-collection-view", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error(`Unable to send request to fetch listing: ${error}`), }; } const listing: Listing = response.data.collection_view_data[0]; if (!listing) { return { success: false, error: new Error("Unable to find listing"), }; } return { success: true, value: listing, }; } /** * Fetch collection bids for a specific collection. * Right now, it ONLY outputs collection bids in DESCENDING order of price. * @param collectionAddress * @returns Array of collection bids or error. */ async getCollectionBidsForProject( collectionAddress: string ): Promise<Result<CollectionBid[], Error>> { const data = JSON.stringify({ condition: { contract_address: collectionAddress, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-collection-bids-for-project", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to get collection bids for project: ${error}` ), }; } const bids: CollectionBid[] = response.data.bids; if (!bids) { return { success: false, error: new Error("No bids found for project"), }; } return { success: true, value: bids, }; } async getCollectionBidsForProjectAndUser( collectionAddress: string, userAddress?: string ): Promise<Result<CollectionBid[], Error>> { if (collectionAddress === "") { throw new Error("Missing collection address"); } if (!userAddress) { userAddress = this.wallet.address; } const data = JSON.stringify({ condition: { contract_address: collectionAddress, buyer_address: userAddress, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-collection-bids-for-project-and-user", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to get collection bids for project and user: ${error}` ), }; } const bids: CollectionBid[] = response.data.bids; if (!bids) { return { success: false, error: new Error("No bids found for project"), }; } return { success: true, value: bids, }; } async getTokenRarityScore( collectionAddress: string, tokenId: string ): Promise<Result<number, Error>> { const getListingResult = await this.getListing(collectionAddress, tokenId); if (!getListingResult.success) { return { success: false, error: getListingResult.error, }; } const listing = getListingResult.value; return { success: true, value: listing.data.rarity_snapshot.hyperspace, }; } /** * List a specific NFT (executes on chain). * @param collectionAddress * @param tokenId * @param listingPriceAvax * @returns Order or error. */ async list( collectionAddress: string, tokenId: string, listingPriceWei: number ): Promise<Result<Order, Error>> { const sellerAddress = this.wallet.address; const tokenAddress = `${collectionAddress}_${tokenId}`; let isApproved; try { isApproved = await getNftApprovalStatus( this.provider, sellerAddress, collectionAddress ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to get NFT approval status: ${error}`), }; } if (!isApproved) { try { await approveNft(this.wallet, collectionAddress); } catch (error: unknown) { return { success: false, error: new Error(`Unable to approve NFT: ${error}`), }; } } const data = JSON.stringify({ condition: { list_tx_args: [ { token_address: tokenAddress, seller_address: sellerAddress as string, metadata: { contractAddress: collectionAddress, tokenId, price: listingPriceWei.toString(), }, }, ], }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/create-list-tx", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to create list transaction: ${error}` ), }; } const order = response.data[0].metadata; if (response.data?.length && order) { let handleSignTransactionResult; try { handleSignTransactionResult = await handleSignTransaction( order, this.wallet ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to handle sign and execute: ${error}`), }; } const signedOrder = JSON.parse( handleSignTransactionResult.transactionBlockBytes ); try { await this.validateSignature(signedOrder); } catch (error: unknown) { return { success: false, error: new Error(`Unable to validate signature: ${error}`), }; } return { success: true, value: order, }; } else { return { success: false, error: new Error(`Unable to create list transaction: ${response.data}`), }; } } /** * Delist a specific NFT (executes on chain). * @param listing * @returns Transaction hash or error. */ async delist( collectionAddress: string, price: number, metadata: any ): Promise<Result<string, Error>> { const data = JSON.stringify({ condition: { delist_tx_args: [ { seller_address: this.wallet.address, token_address: collectionAddress, price: (price * 10 ** 18).toString(), metadata: metadata, }, ], }, }); const requestConfig = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/create-delist-tx", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestConfig); } catch (error: any) { return { success: false, error: new Error( `Unable to send request to create delist transaction: ${error}` ), }; } const encodedTransaction = response.data[0].byte_string; if (response.data?.length && encodedTransaction) { let transactionReceipt; try { transactionReceipt = await handleSignAndExecute( this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, } ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to handle sign and execute`), }; } if (transactionReceipt.transactionHash) { return { success: true, value: transactionReceipt.transactionHash, }; } else { return { success: false, error: new Error(`Invalid transaction receipt`), }; } } else { return { success: false, error: new Error(`Unable to create delist transaction`), }; } } // async updateListing( // collectionAddress: string, // tokenId: string, // newListingPriceWei: number // ) { // const sellerAddress = this.wallet.address; // const tokenAddress = `${collectionAddress}_${tokenId}`; // let isApproved; // try { // isApproved = await getNftApprovalStatus( // this.provider, // sellerAddress, // collectionAddress // ); // } catch (error: unknown) { // return { // success: false, // error: new Error(`Unable to get NFT approval status: ${error}`), // }; // } // const data = JSON.stringify({ // condition: { // list_tx_args: [ // { // token_address: tokenAddress, // seller_address: sellerAddress as string, // metadata: { // contractAddress: collectionAddress, // tokenId, // price: newListingPriceWei.toString(), // }, // }, // ], // }, // }); // const requestArguments = { // method: "post", // url: "https://avax.api.hyperspace.xyz/rest/create-update-listing-tx", // headers: { // "Content-Type": "application/json", // Authorization: this.apiKey, // }, // data: data, // }; // let response; // try { // response = await axios.request(requestArguments); // } catch (error: unknown) { // return { // success: false, // error: new Error( // `Unable to send request to create update listing transaction: ${error}` // ), // }; // } // console.log(response.data); // } /** * Creates a collection bid (), executes on chain * @param contractAddress * @param bidAmount * @returns */ async collectionBid( contractAddress: string, bidAmountWei: number ): Promise<Result<Order, Error>> { const bidderAddress = this.wallet.address; const wavaxBalance = await getWavaxBalance(this.provider, bidderAddress); let isApproved; try { isApproved = await getWavaxApprovalStatus( this.provider, bidderAddress, wavaxBalance ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to fetch NFT approval status: ${error}`), }; } if (!isApproved) { try { await approveWavax(this.wallet); } catch (error: unknown) { return { success: false, error: new Error(`Unable to approve NFT: ${error}`), }; } } const data = JSON.stringify({ condition: { price: bidAmountWei, buyer_address: bidderAddress, metadata: { contractAddress, price: bidAmountWei.toString(), }, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/create-collection-bid-tx", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to create collection bid transaction: ${error}` ), }; } const order = response.data[0].metadata; if (response.data?.length && order) { let handleSignTransactionResult; try { handleSignTransactionResult = await handleSignTransaction( order, this.wallet ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to handle sign and execute: ${error}`), }; } const signedOrder = JSON.parse( handleSignTransactionResult.transactionBlockBytes ); try { await this.validateSignature(signedOrder); } catch (error: unknown) { return { success: false, error: new Error(`Unable to validate signature: ${error}`), }; } return { success: true, value: order, }; } else { return { success: false, error: new Error( `Unable to create collection bid transaction: ${response.data}` ), }; } } /** * Cancel a collection bid (executes on-chain). * @param collection_bid * @returns Transaction hash or error. */ async cancelCollectionBid( collectionBid: CollectionBid ): Promise<Result<string, Error>> { const data = JSON.stringify({ condition: { buyer_address: this.wallet.address, price: collectionBid.price, metadata: collectionBid.metadata, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/create-cancel-collection-bid-tx", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to create cancel collection bid transaction: ${error}` ), }; } const encodedTransaction = response.data[0].byte_string; if (response.data?.length && encodedTransaction) { let transactionReceipt; try { transactionReceipt = await handleSignAndExecute( this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, } ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to handle sign and execute: ${error}`), }; } if (transactionReceipt.transactionHash) { return { success: true, value: transactionReceipt.transactionHash, }; } return { success: false, error: new Error(`Invalid transaction receipt`), }; } else { return { success: false, error: new Error(`Unable to create cancel collection bid transaction`), }; } } async updateCollectionBid() {} /** * Buy (create buy transaction) a specific NFT on-chain. * @param collectionAddress * @param tokenId * @returns Transaction hash or error. */ async buy( collectionAddress: string, tokenId: string ): Promise<Result<Buy, Error>> { const getListingResult = await this.getListing(collectionAddress, tokenId); if (!getListingResult.success) { return { success: false, error: getListingResult.error, }; } const listing = getListingResult.value; const listingPriceAvax = listing.price.raw_price; const listingMetadata = listing.data.listing_snapshot.metadata; if (!listingPriceAvax) { return { success: false, error: new Error("Unable to find listing price"), }; } if (!listingMetadata) { return { success: false, error: new Error("Unable to find listing metadata"), }; } const buyTransactionArguments = [ { buyer_address: this.wallet.address.toString(), token_address: listing.data.token_address, price: listingPriceAvax, metadata: listingMetadata, }, ]; const data = JSON.stringify({ condition: { buy_tx_args: buyTransactionArguments, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/create-buy-tx", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to create buy transaction: ${error}` ), }; } const totalAmountToPay = [ BigInt(listingMetadata.event_log.erc20TokenAmount), ...listingMetadata.event_log.fees.map((fee: any) => BigInt(fee.amount)), ].reduce((a, b) => a + b); if (response.data?.length && response.data[0].byte_string) { const encodedTransaction = response.data[0].byte_string; let transactionReceipt; try { transactionReceipt = await handleSignAndExecute( this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, value: totalAmountToPay, } ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to handle sign and execute: ${error}`), }; } const transactionHash = transactionReceipt.transactionHash; if (transactionReceipt.status == 1 && transactionHash) { return { success: true, value: { transactionHash: transactionReceipt.transactionHash, price: listingPriceAvax, }, }; } return { success: false, error: new Error(`Invalid transaction receipt`), }; } else { return { success: false, error: new Error(`Unable to create buy transaction`), }; } } /** * Sell (create accept collection bid transaction) a specific NFT on-chain. * By default, it will accept the highest bid for the NFT. * If you want to accept a specific bid, pass the bid amount as a parameter. * @param collectionAddress * @param tokenId * @returns Transaction hash, price, and fee or error. */ async executeSell( collectionAddress: string, tokenId: string // bidAmount?: number ): Promise<Result<Sale, Error>> { const sellerAddress = this.wallet.address; let isApproved; try { isApproved = await getNftApprovalStatus( this.provider, sellerAddress, collectionAddress ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to fetch NFT approval status: ${error}`), }; } if (!isApproved) { try { await approveNft(this.wallet, collectionAddress); } catch (error: unknown) { return { success: false, error: new Error(`Unable to approve NFT: ${error}`), }; } } const getCollectionBidsForProjectResult = await this.getCollectionBidsForProject(collectionAddress); if (!getCollectionBidsForProjectResult.success) { return { success: false, error: getCollectionBidsForProjectResult.error, }; } const bid = getCollectionBidsForProjectResult.value[0]; const bidPrice = bid.price; const bidFee = bid.fee; const bidMetadata = bid.metadata; if (!bidPrice) { return { success: false, error: new Error(`No collection bid price found for highest bid`), }; } if (!bidFee) { return { success: false, error: new Error(`No collection bid fee found for highest bid`), }; } if (!bidMetadata) { return { success: false, error: new Error(`No collection bid metadata found for highest bid`), }; } const data = JSON.stringify({ condition: { token_address: `${collectionAddress}_${tokenId}`, price: bidPrice, seller_address: sellerAddress, metadata: bidMetadata, }, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/create-accept-collection-bid-tx", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to create accept collection bid transaction: ${error}` ), }; } if (response.data?.length && response.data[0].byte_string) { const encodedTransaction = response.data[0].byte_string; let transactionReceipt; try { transactionReceipt = await handleSignAndExecute( this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, } ); } catch (error: unknown) { return { success: false, error: new Error(`Unable to handle sign and execute: ${error}`), }; } const transactionHash = transactionReceipt.transactionHash; if (transactionReceipt.status == 1 && transactionHash) { return { success: true, value: { transactionHash, price: bidPrice, fee: bidFee, }, }; } return { success: false, error: new Error(`Invalid transaction receipt`), }; } else { return { success: false, error: new Error(`Unable to create accept collection bid transaction`), }; } } async userBids(contractAddress?: string, tokenId?: string) { const userAddress = this.wallet.address; try { const data = JSON.stringify({ condition: { owner: userAddress, listing_type: "NORMAL", project_ids: [ { project_id: "c43f2070-8d90-40f2-a81a-d564889263ee", }, ], }, }); const requestConfig = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-marketplace-snapshots", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; const response = await axios.request(requestConfig); const marketplace_snapshots = response.data.marketplace_snapshots; console.log(marketplace_snapshots); } catch (error: any) { console.log(error); return { digest: null, errors: error.message, }; } } async userListings() { const userAddress = this.wallet.address; // "condition": { // "token_address": "string", // "project_id": "string", // "action_type": "string", // "buyer_address": "string", // "seller_address": "string", // "marketplace_programs": [ // {} // ], // "populate_snapshot": true // } try { const data = JSON.stringify({ condition: { buyer_address: userAddress, }, }); const requestConfig = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-user-listings", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; const res = await axios.request(requestConfig); } catch (error: any) { console.log(error); return { digest: null, errors: error.message, }; } } // TODO: Promise<Result<any, Error>> async validateSignature(signedOrder: any) { const data = JSON.stringify({ order: signedOrder, }); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/validate-signature", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { throw new Error(`Unable to validate signature: ${error}`); } return response; } /** * Get all NFTs owned by a specific user. * If no user address is provided, it will fetch NFTs owned by this client's wallet address. */ async getUserOwnedNfts( userAddress?: string, projectId?: string, pagination?: PaginationConfig ): Promise<Result<Nft[], Error>> { const data = JSON.stringify( projectId ? { condition: { owner: userAddress ? userAddress : this.wallet.address, project_ids: [ { project_id: projectId, }, ], }, pagination_info: pagination, } : { condition: { owner: userAddress, }, pagination_info: pagination, } ); const requestArguments = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-marketplace-snapshots", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data, }; let response; try { response = await axios.request(requestArguments); } catch (error: unknown) { return { success: false, error: new Error(`Unable to send request to fetch listings: ${error}`), }; } return { success: true, value: response.data.marketplace_snapshots }; } async getCollectionStats( projectId?: string, orderBy?: OrderConfig[], pagination?: PaginationConfig ): Promise<Result<ProjectStats[], Error>> { const data = JSON.stringify( projectId ? { condition: { project_ids: [projectId], }, order_by: orderBy, pagination_info: pagination, } : { order_by: orderBy, pagination_info: pagination, } ); const args = { method: "post", url: "https://avax.api.hyperspace.xyz/rest/get-project-stats", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data, }; let res; try { res = await axios.request(args); } catch (error: unknown) { return { success: false, error: new Error( `Unable to send request to get collection stats: ${error}` ), }; } return { success: true, value: res.data.project_stats }; } async getCollectionActivity( collection: string, actionTypes?: string[], pagination?: PaginationConfig ) { const data = JSON.stringify({ condition: { projects: [ { project_id: collection, }, ], action_types: actionTypes, }, pagination_info: pagination, }); const config = { method: "post", maxBodyLength: Infinity, url: "https://avax.api.hyperspace.xyz/rest/get-collection-activity", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; return (await axios.request(config)).data; } async getUserActivity( walletAddress: string, actionTypes?: string[], pagination?: PaginationConfig ) { const data = JSON.stringify({ condition: { user_address: walletAddress, action_types: actionTypes, }, pagination_info: pagination, }); const config = { method: "post", maxBodyLength: Infinity, url: "https://avax.api.hyperspace.xyz/rest/get-user-activity", headers: { "Content-Type": "application/json", Authorization: this.apiKey, }, data: data, }; return (await axios.request(config)).data; } }