UNPKG

@gotake/gotake-sdk

Version:

SDK for interacting with GoTake blockchain contracts

470 lines (469 loc) 18.6 kB
import { ethers } from 'ethers'; import { JsonRpcProvider } from '@ethersproject/providers'; import { NetworkId, getNetworkById, getNetworkByName } from './utils/index.js'; import { hasEnvironmentVariables, getEnvironmentVariable } from './utils/environment.js'; import { AccountApi, IPNFTApi, VideoApi, VideoPaymentApi } from './api/index.js'; import { VideoStatus } from './types.js'; import { StatusTracker } from './utils/statusTracker.js'; /** * GoTake SDK - Main SDK class for interacting with GoTake contracts and services */ export class GoTakeSDK { /** * Create SDK instance * @param options Initialization options */ constructor(options = {}) { // Determine network this._networkConfig = this._resolveNetwork(options.network); this._networkId = this._networkConfig.id; // Set up Provider this._provider = this._resolveProvider(options.provider, this._networkConfig); // Set up Signer (synchronous fallback) this._signer = this._resolveSigner(options.signer); // Set up API config this._apiConfig = { endpoint: options.apiEndpoint || 'https://api.goodtake.io', // Default API endpoint headers: { 'Authorization': `Bearer ${options.apiKey || ''}` // JWT token if provided } }; // Initialize APIs this.account = new AccountApi(this._provider, this._signer); this.ipnft = new IPNFTApi(this._provider, this._signer); this.video = new VideoApi(this._apiConfig); this.videoPayment = new VideoPaymentApi(this._provider, this._signer); // Initialize status tracker this._statusTracker = StatusTracker.getInstance(); } /** * Get current network ID */ get networkId() { return this._networkId; } /** * Get Provider */ get provider() { return this._provider; } /** * Get Signer */ get signer() { return this._signer; } /** * Get current user address */ async getAddress() { return this._signer.getAddress(); } /** * Check if user has a TBA and create one if not exists * @param options Transaction options * @returns Promise resolving to TBA address and whether it was newly created */ async checkAndCreateTBA(options) { try { // Create parameters const salt = "0x0000000000000000000000000000000000000000000000000000000000000000"; const tokenId = 0; // Use default token ID const tokenContract = "0xE71014657C0AC1bF1Bbc3a85E0b21087A83411f4"; // Configured NFT contract address // Calculate TBA address const tbaAddress = await this.account.computeTBAAddress({ tokenContract, tokenId, salt }); // Check if account already exists const code = await this.account.provider.getCode(tbaAddress); const exists = code !== '0x'; if (!exists) { // Create TBA const result = await this.account.createTBA({ tokenContract, tokenId, salt }, options); return { tbaAddress: result.tbaAddress, isNew: true }; } return { tbaAddress, isNew: false }; } catch (error) { console.error('Error checking/creating TBA:', error); throw error; } } /** * Upload video to the platform * @param file Video file to upload * @param metadata Video metadata * @returns Promise resolving to video ID */ async uploadVideo(file, metadata) { return this.video.uploadVideo(file, metadata); } /** * Get video status * @param videoId Video ID * @returns Promise resolving to video status information */ async getVideoStatus(videoId) { return this.video.getVideoInfo(videoId); } /** * Subscribe to video status updates * @param videoId Video ID * @param callback Function to be called when status updates * @returns Subscription object */ subscribeToVideoStatus(videoId, callback) { return this._statusTracker.subscribe(videoId, callback); } /** * Poll video status at regular intervals * @param videoId Video ID * @param options Poll options * @returns Poll controller */ pollVideoStatus(videoId, options) { return this._statusTracker.startPolling(videoId, () => this.getVideoStatus(videoId), options); } /** * Mint IP NFT for a video * @param videoId Video ID * @param options Transaction options * @returns Promise resolving to mint result */ async mintIPNFT(videoId, options) { var _a, _b; try { // Get video status const status = await this.getVideoStatus(videoId); // Check if video is ready for minting if (status.status !== VideoStatus.READY && status.status !== VideoStatus.MINT_READY) { throw new Error(`Video not ready for minting. Current status: ${status.status}`); } // Get TBA address const { tbaAddress } = await this.checkAndCreateTBA(options); // Update status this._statusTracker.updateStatus(videoId, { status: VideoStatus.MINTING }); // Create metadata const uri = ((_a = status.assetInfo) === null || _a === void 0 ? void 0 : _a.playbackUrl) || `video://${videoId}`; const title = ((_b = status.assetInfo) === null || _b === void 0 ? void 0 : _b.assetId) || `Video ${videoId}`; const description = status.message || ''; const userAddress = await this.account.getAddress(); // Mint NFT const result = await this.ipnft.mint(tbaAddress, { title, description, thumbnailUrl: uri, creator: userAddress }, options); // Get NFT details const nftDetails = await this.ipnft.getNFTDetails(result.tokenId); // Format result const mintResult = { tokenId: result.tokenId.toString(), transactionHash: result.tx.hash, owner: nftDetails.owner, metadata: { seriesTitle: nftDetails.seriesTitle, description: nftDetails.description, totalSeasons: nftDetails.totalSeasons, totalEpisodes: nftDetails.totalEpisodes, genres: nftDetails.genres, creators: nftDetails.creators, createdAt: nftDetails.createdAt, posterUri: nftDetails.posterUri } }; // Update status this._statusTracker.updateStatus(videoId, { status: VideoStatus.COMPLETED, mintInfo: { tokenId: mintResult.tokenId, transactionHash: mintResult.transactionHash } }); return mintResult; } catch (error) { // Update status with error this._statusTracker.updateStatus(videoId, { status: VideoStatus.ERROR, error: { code: 'MINT_ERROR', message: error instanceof Error ? error.message : 'Unknown minting error' } }); throw error; } } /** * Upload video and mint NFT when ready * @param file Video file to upload * @param metadata Video metadata * @param options Options including status callback and auto-mint flag * @returns Promise resolving to video ID */ async uploadAndMintWhenReady(file, metadata, options) { // Check TBA await this.checkAndCreateTBA(options === null || options === void 0 ? void 0 : options.transactionOptions); // Upload video const videoId = await this.uploadVideo(file, metadata); // Set up status callback if (options === null || options === void 0 ? void 0 : options.statusCallback) { this.subscribeToVideoStatus(videoId, options.statusCallback); } // Auto-mint if requested if (options === null || options === void 0 ? void 0 : options.autoMint) { this.pollVideoStatus(videoId, { interval: 5000, stopCondition: (status) => status.status === VideoStatus.READY || status.status === VideoStatus.ERROR, callback: async (status) => { if (status.status === VideoStatus.READY) { try { await this.mintIPNFT(videoId, options.transactionOptions); } catch (error) { console.error('Auto-mint failed:', error); } } } }); } return videoId; } // VideoPayment Convenience Methods /** * Purchase content with simplified interface * @param contentId Content ID to purchase * @param paymentMethod Payment method (ETH or ERC20) * @param tokenAddress Token address (required for ERC20 payments) * @param options Transaction options * @returns Purchase result */ async purchaseContent(contentId, paymentMethod, tokenAddress, options) { return this.videoPayment.purchaseContent(contentId, paymentMethod, tokenAddress, options); } /** * Check if current user has view permission for content * @param contentId Content ID * @returns True if user has valid view permission */ async hasViewPermission(contentId) { return this.videoPayment.hasViewPermission(contentId); } /** * Get current user's permission details for content * @param contentId Content ID * @returns View permission details */ async getMyPermissions(contentId) { return this.videoPayment.getMyPermissions(contentId); } /** * Get content information including pricing * @param contentId Content ID * @returns Content information */ async getContentInfo(contentId) { return this.videoPayment.getContentInfo(contentId); } /** * Switch network * @param network Network ID or name */ async switchNetwork(network) { const newNetworkConfig = this._resolveNetwork(network); this._networkId = newNetworkConfig.id; this._networkConfig = newNetworkConfig; // Update Provider this._provider = new JsonRpcProvider(newNetworkConfig.rpcUrl); // Update Signer's Provider if (this._signer instanceof ethers.Wallet) { this._signer = this._signer.connect(this._provider); } // Update API connections this.account.updateConnection(this._provider, this._signer); this.ipnft.updateConnection(this._provider, this._signer); // Video API doesn't have updateConnection method } /** * Resolve network configuration * @param network Network ID or name * @returns Network configuration */ _resolveNetwork(network) { if (!network) { // Default to Base Sepolia return getNetworkById(NetworkId.BASE_SEPOLIA); } if (typeof network === 'number' || !isNaN(Number(network))) { // Look up by ID const networkId = typeof network === 'number' ? network : Number(network); const config = getNetworkById(networkId); if (!config) { throw new Error(`Network ID ${networkId} not supported`); } return config; } else { // Look up by name const config = getNetworkByName(network); if (!config) { throw new Error(`Network "${network}" not supported`); } return config; } } /** * Resolve Provider * @param provider Provider or RPC URL * @param networkConfig Network configuration * @returns Provider instance */ _resolveProvider(provider, networkConfig) { if (!provider) { // Use network config's RPC URL return new JsonRpcProvider(networkConfig.rpcUrl); } if (typeof provider === 'string') { // Assume it's an RPC URL return new JsonRpcProvider(provider); } // Already a Provider instance return provider; } /** * Resolve Signer * @param signer Signer, private key, or environment variable name * @returns Signer instance */ _resolveSigner(signer) { if (!signer) { // Check if we're in Node.js environment and can use environment variables if (hasEnvironmentVariables()) { const privateKey = getEnvironmentVariable('PRIVATE_KEY'); if (privateKey) { return new ethers.Wallet(privateKey, this._provider); } else { throw new Error(`No signer provided and no PRIVATE_KEY environment variable found. In Node.js, set PRIVATE_KEY in .env file. In browser, provide signer explicitly.`); } } else { throw new Error(`No signer provided. In browser environment, you must provide a signer (e.g., from MetaMask, WalletConnect, etc.)`); } } if (typeof signer === 'string') { // Could be private key or environment variable name if (signer.startsWith('0x')) { // Assume it's a private key return new ethers.Wallet(signer, this._provider); } else { // Assume it's an environment variable name (only works in Node.js) if (hasEnvironmentVariables()) { const privateKey = getEnvironmentVariable(signer); if (privateKey) { return new ethers.Wallet(privateKey, this._provider); } else { throw new Error(`Failed to load private key from environment variable ${signer}`); } } else { throw new Error(`Environment variables not available in browser. Please provide private key directly.`); } } } // Already a Signer instance return signer; } /** * Get current gas price recommendations * @param options Optional configuration * @returns Gas price data including maxFeePerGas and maxPriorityFeePerGas */ async getGasPrice(options) { try { // Default multipliers const gasPriceMultiplier = (options === null || options === void 0 ? void 0 : options.multiplier) || 1.2; const priorityFeeMultiplier = (options === null || options === void 0 ? void 0 : options.priorityMultiplier) || 1.5; // Get latest block const latestBlock = await this._provider.getBlock('latest'); // Check if network supports EIP-1559 if (latestBlock.baseFeePerGas) { const baseFeePerGas = latestBlock.baseFeePerGas; // Get priority fee data let maxPriorityFeePerGas; try { // Get network config for default priority fee const networkDefaultPriorityFee = this._networkConfig.maxPriorityFeePerGas; // Use the network's configured priority fee instead of feeData maxPriorityFeePerGas = ethers.utils.parseUnits(networkDefaultPriorityFee, "gwei"); // If priority fee from network config is 0, we can get it from feeData as backup if (maxPriorityFeePerGas.isZero()) { const feeData = await this._provider.getFeeData(); maxPriorityFeePerGas = feeData.maxPriorityFeePerGas || ethers.utils.parseUnits("0", "gwei"); } } catch (e) { // Default priority fee if not available - use 0 as default fallback maxPriorityFeePerGas = ethers.utils.parseUnits("0", "gwei"); } // Apply multipliers const adjustedPriorityFee = maxPriorityFeePerGas.mul(Math.floor(priorityFeeMultiplier * 100)).div(100); // Calculate maxFeePerGas = baseFeePerGas * multiplier + maxPriorityFeePerGas const adjustedMaxFee = baseFeePerGas .mul(Math.floor(gasPriceMultiplier * 100)) .div(100) .add(adjustedPriorityFee); return { baseFeePerGas, maxFeePerGas: adjustedMaxFee, maxPriorityFeePerGas: adjustedPriorityFee }; } else { // For non-EIP-1559 networks const gasPrice = await this._provider.getGasPrice(); // Apply multiplier to gas price const adjustedGasPrice = gasPrice.mul(Math.floor(gasPriceMultiplier * 100)).div(100); return { maxFeePerGas: adjustedGasPrice, gasPrice: adjustedGasPrice }; } } catch (error) { console.warn("Failed to get gas price data:", error); // Return basic gas price as fallback const gasPrice = await this._provider.getGasPrice(); return { maxFeePerGas: gasPrice, gasPrice: gasPrice }; } } } // Export types and utilities export { NetworkId, getNetworkById, getNetworkByName } from './utils/index.js'; // Export types export { VideoStatus } from './types.js'; // Export these modules fully export * from './api/index.js'; export * from './wrappers/index.js';