@gotake/gotake-sdk
Version:
SDK for interacting with GoTake blockchain contracts
470 lines (469 loc) • 18.6 kB
JavaScript
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';