UNPKG

hyperspace-sdk

Version:

An unofficial SDK for Hyperspace NFT Marketplace on Avalanche

1,140 lines (1,139 loc) 38.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Hyperspace = void 0; const ethers_1 = require("ethers"); const providers_1 = require("@ethersproject/providers"); const axios_1 = __importDefault(require("axios")); const helpers_1 = require("./utils/helpers"); class Hyperspace { constructor(apiKey, rpcHttpUrl, privateKey) { this.gasPriceMultiplier = 1; this.gasLimitMultiplier = 1; this.apiKey = apiKey; this.provider = new providers_1.JsonRpcBatchProvider(rpcHttpUrl); const wallet = new ethers_1.Wallet(`0x${privateKey}`); this.wallet = wallet.connect(this.provider); } getGasPriceMultiplier() { return this.gasPriceMultiplier; } updateGasPriceMultiplier(newGasPriceMultiplier) { 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() { return this.gasLimitMultiplier; } updateGasLimitMultiplier(newGasLimitMultiplier) { 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, }; } async getListingsForProject(projectId, paginationInfo) { 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_1.default.request(requestArguments); } catch (error) { return { success: false, error: new Error(`Unable to send request to fetch listings: ${error}`), }; } const listings = 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? async getListingsForUser(userAddress, projectId, paginationInfo) { 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_1.default.request(requestArguments); } catch (error) { return { success: false, error: new Error(`Unable to send request to fetch listings: ${error}`), }; } const listings = 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, tokenId) { 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_1.default.request(requestArguments); } catch (error) { return { success: false, error: new Error(`Unable to send request to fetch listing: ${error}`), }; } const 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) { 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_1.default.request(requestArguments); } catch (error) { return { success: false, error: new Error(`Unable to send request to get collection bids for project: ${error}`), }; } const bids = response.data.bids; if (!bids) { return { success: false, error: new Error("No bids found for project"), }; } return { success: true, value: bids, }; } async getCollectionBidsForProjectAndUser(collectionAddress, userAddress) { 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_1.default.request(requestArguments); } catch (error) { return { success: false, error: new Error(`Unable to send request to get collection bids for project and user: ${error}`), }; } const bids = response.data.bids; if (!bids) { return { success: false, error: new Error("No bids found for project"), }; } return { success: true, value: bids, }; } async getTokenRarityScore(collectionAddress, tokenId) { 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, tokenId, listingPriceWei) { const sellerAddress = this.wallet.address; const tokenAddress = `${collectionAddress}_${tokenId}`; let isApproved; try { isApproved = await (0, helpers_1.getNftApprovalStatus)(this.provider, sellerAddress, collectionAddress); } catch (error) { return { success: false, error: new Error(`Unable to get NFT approval status: ${error}`), }; } if (!isApproved) { try { await (0, helpers_1.approveNft)(this.wallet, collectionAddress); } catch (error) { 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, 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_1.default.request(requestArguments); } catch (error) { 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 (0, helpers_1.handleSignTransaction)(order, this.wallet); } catch (error) { 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) { 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, price, metadata) { 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_1.default.request(requestConfig); } catch (error) { 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 (0, helpers_1.handleSignAndExecute)(this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, }); } catch (error) { 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, bidAmountWei) { const bidderAddress = this.wallet.address; const wavaxBalance = await (0, helpers_1.getWavaxBalance)(this.provider, bidderAddress); let isApproved; try { isApproved = await (0, helpers_1.getWavaxApprovalStatus)(this.provider, bidderAddress, wavaxBalance); } catch (error) { return { success: false, error: new Error(`Unable to fetch NFT approval status: ${error}`), }; } if (!isApproved) { try { await (0, helpers_1.approveWavax)(this.wallet); } catch (error) { 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_1.default.request(requestArguments); } catch (error) { 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 (0, helpers_1.handleSignTransaction)(order, this.wallet); } catch (error) { 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) { 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) { 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_1.default.request(requestArguments); } catch (error) { 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 (0, helpers_1.handleSignAndExecute)(this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, }); } catch (error) { 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, tokenId) { 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_1.default.request(requestArguments); } catch (error) { 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) => 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 (0, helpers_1.handleSignAndExecute)(this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, value: totalAmountToPay, }); } catch (error) { 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, tokenId // bidAmount?: number ) { const sellerAddress = this.wallet.address; let isApproved; try { isApproved = await (0, helpers_1.getNftApprovalStatus)(this.provider, sellerAddress, collectionAddress); } catch (error) { return { success: false, error: new Error(`Unable to fetch NFT approval status: ${error}`), }; } if (!isApproved) { try { await (0, helpers_1.approveNft)(this.wallet, collectionAddress); } catch (error) { 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_1.default.request(requestArguments); } catch (error) { 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 (0, helpers_1.handleSignAndExecute)(this.wallet, this.gasPriceMultiplier, this.gasLimitMultiplier, { data: encodedTransaction, }); } catch (error) { 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, tokenId) { 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_1.default.request(requestConfig); const marketplace_snapshots = response.data.marketplace_snapshots; console.log(marketplace_snapshots); } catch (error) { 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_1.default.request(requestConfig); } catch (error) { console.log(error); return { digest: null, errors: error.message, }; } } // TODO: Promise<Result<any, Error>> async validateSignature(signedOrder) { 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_1.default.request(requestArguments); } catch (error) { 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, projectId, pagination) { 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_1.default.request(requestArguments); } catch (error) { 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, orderBy, pagination) { 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_1.default.request(args); } catch (error) { 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, actionTypes, pagination) { 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_1.default.request(config)).data; } async getUserActivity(walletAddress, actionTypes, pagination) { 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_1.default.request(config)).data; } } exports.Hyperspace = Hyperspace;