UNPKG

@nitin-spheron/desub-js

Version:

Typescript library for working with the decentralised subscription contracts in Spheron

727 lines (686 loc) 23.8 kB
import { Vendor } from '.' import { ethers } from 'ethers' import Deployed from './abstracts/deployed' import { PAYMENT_ABI, ERC20_ABI, SUBSCRIPTION_PAYMENT_ABI, SUBSCRIPTION_DATA_ABI, SUBSCRIPTION_NATIVE_PAYMENT_ABI, SUBSCRIPTION_MANTLE_PAYMENT_ABI, } from './constants' import { INVALID_BICONOMY_KEY, TRANSACTION_FAILED } from './errors' import { SubscriptionParameters, TokenData, TxResponse } from './interfaces' export default class SubscriptionContract extends Deployed { /** * Creates an instance of SubscriptionContract. * @param vendor - Instance of a Vendor class */ constructor(vendor: Vendor) { super( vendor, PAYMENT_ABI, ERC20_ABI, SUBSCRIPTION_PAYMENT_ABI, SUBSCRIPTION_DATA_ABI, SUBSCRIPTION_NATIVE_PAYMENT_ABI, SUBSCRIPTION_MANTLE_PAYMENT_ABI, ) } /** * Updates the address of the vault/escrow account. * @param escrowAddress - Address of the escrow contract (vault) * @returns A promise that resolves to a transaction response. */ async updateEscrow(escrowAddress: string): Promise<TxResponse> { return await this.paymentsContract?.functions.updateEscrow(escrowAddress) } /** * Updates the address of the staked token. * @param stakedTokenAddress - Address of the staked token. * @returns A promise that resolves to a transaction response. */ async updateStakedToken(stakedTokenAddress: string): Promise<TxResponse> { return await this.subscriptionDataContract?.functions.updateStakedToken(stakedTokenAddress) } /** * Update the discount slabs, which can only be called by the governance account. * @param discountSlabs - An array of prices for discount slabs * @param percents - An array of percentage of each slab * @returns A Promise that resolves with the transaction response */ async updateDiscountSlabs(discountSlabs: string[], percents: string[]): Promise<TxResponse> { const vendor = this.vendor const convertedDiscountSlabs = vendor.convertStringArrayToBigNumberArray(discountSlabs) const convertedPercents = vendor.convertStringArrayToBigNumberArray(percents) return this.subscriptionDataContract?.functions.updateDiscountSlabs(convertedDiscountSlabs, convertedPercents) } /** * Enable discounts, which can only be called by the owner. * @param stakingManagerAddress - The address of the staking manager * @returns A Promise that resolves with the transaction response */ async enableDiscounts(stakingManagerAddress: string): Promise<TxResponse> { return this.subscriptionDataContract?.functions.enableDiscounts(stakingManagerAddress) } /** * Disable discounts, which can only be called by the owner. * @returns A Promise that resolves with the transaction response */ async disableDiscounts(): Promise<TxResponse> { return this.subscriptionDataContract?.functions.disableDiscounts() } /** * Update the governance contract address, which can only be called by the owner. * @param governanceAddress - The address of the governance contract * @returns A Promise that resolves with the transaction response */ async setGovernanceAddress(governanceAddress: string): Promise<TxResponse> { return this.subscriptionDataContract?.functions.setGovernanceAddress(governanceAddress) } /** * This method can only be called by the contract owner to update the data contract address. * * @param dataContractAddress - Address of the data contract. */ async setDataContract(dataContractAddress: string): Promise<TxResponse> { return await this.subscriptionPaymentContract?.functions.updateDataContract(dataContractAddress) } /** * This method can only be called by the contract owner to set new owners. * * @param newOwners - Array of addresses of new owners. */ async setManagers(newOwners: string[]): Promise<TxResponse> { return await this.subscriptionDataContract?.functions.setManagers(newOwners) } /** * Update approval for ERC-20 token. * Do not use this function without frontend. * * @param approvalAmount - New approval amount. */ async setNewApprovals(approvalAmount: string): Promise<TxResponse> { const weiAmount = this.vendor.convertToWei(approvalAmount, this.tokenPrecision || 18) return await this.erc20Contract?.functions.approve(this.subscriptionPaymentContract?.address, weiAmount) } /** * Token approval and user deposit in one transaction. * Do not use this function without frontend. * @param approvalAmount - Amount of tokens to approve and deposit. */ async approveAndDeposit(approvalAmount: string): Promise<TxResponse> { try { const weiAmount = this.vendor.convertToWei(approvalAmount, this.tokenPrecision || 18) await this.erc20Contract?.functions.approve(this.subscriptionPaymentContract?.address, weiAmount) return this.subscriptionPaymentContract?.functions.userDeposit(this.erc20Contract?.address ?? '', weiAmount) } catch (error) { if (error instanceof Error) { throw new Error(`Transaction failed: ${error.message}`) } else { throw new Error(TRANSACTION_FAILED) } } } async approveAndDepositWithGasLimit(approvalAmount: string): Promise<TxResponse> { try { const weiAmount = this.vendor.convertToWei(approvalAmount, this.tokenPrecision || 18) await this.erc20Contract?.functions.approve(this.subscriptionPaymentContract?.address, weiAmount, { gasLimit: 300000, }) return await this.subscriptionPaymentContract?.functions.userDeposit( this.erc20Contract?.address ?? '', weiAmount, { gasLimit: 300000, }, ) } catch (error) { if (error instanceof Error) { throw new Error(`Transaction failed: ${error.message}`) } else { throw new Error(TRANSACTION_FAILED) } } } /** * Update approval for ERC-20 token. * Do not use this function without frontend. * * @param approvalAmount - New approval amount. * @param chainId - Chain ID. */ async gasslessApproval(approvalAmount: string, chainId: number): Promise<TxResponse> { if (!this.vendor.biconomy) { throw new Error(INVALID_BICONOMY_KEY) } const weiAmount = this.vendor.convertToWei(approvalAmount, this.tokenPrecision || 18) const abiEncodedApprove = this.vendor.abiEncodeErc20Functions('approve', [ this.subscriptionPaymentContract?.address, weiAmount, ]) const userAddress = await this.vendor.signer.getAddress() const nonce = await this.getNonceForGaslessERC20(userAddress) const signedMessage = await this.vendor.signedMessageForTx( userAddress, nonce, abiEncodedApprove, this.erc20Contract?.address || '', chainId, ) const rsv = this.vendor.getSignatureParameters(signedMessage) return await this.sendRawBiconomyERC20Transaction(userAddress, abiEncodedApprove, rsv) } /** * @remarks * Gasless user deposit/withdrawal * Do not use this function without a frontend * * @param a - amount of deposit/withdrawal * @param contract - subscription payment contract instance * @param erc20 - ERC20 contract instance * @param biconomy - Biconomy instance */ async gaslessUserAction( a: string, contract: ethers.Contract, erc20: ethers.Contract, biconomy: any, ): Promise<TxResponse> { if (!biconomy) throw new Error(INVALID_BICONOMY_KEY) const wei = this.vendor.convertToWei(a, this.tokenPrecision || 18) const ethersProvider = new ethers.providers.Web3Provider(biconomy) const userAddress = await this.vendor.signer.getAddress() const { data } = await contract.populateTransaction.userDeposit(erc20?.address || '', wei) const txParams = { data, to: contract?.address || '', from: userAddress, signatureType: 'EIP712_SIGN', } return await ethersProvider.send('eth_sendTransaction', [txParams]) } /** * @remarks * Gasless user deposit * Do not use this function without a frontend * * @param a - deposit amount. * @param contract - subscription payment contract instance * @param erc20 - ERC20 contract instance * @param biconomy - Biconomy instance */ async gaslessUserDeposit( a: string, contract: ethers.Contract, erc20: ethers.Contract, biconomy: any, ): Promise<TxResponse> { return await this.gaslessUserAction(a, contract, erc20, biconomy) } /** * @remarks * Gasless user withdrawal * Do not use this function without a frontend * * @param a - withdrawal amount. * @param contract - subscription payment contract instance * @param erc20 - ERC20 contract instance * @param biconomy - Biconomy instance */ async gaslessUserWithdraw( a: string, contract: ethers.Contract, erc20: ethers.Contract, biconomy: any, ): Promise<TxResponse> { return await this.gaslessUserAction(a, contract, erc20, biconomy) } /** * Update approval for ERC-20 token. * This function requires a frontend to be used. * * @param approvalAmount - New approval amount in string format. * @param tokenName - ERC-20 token name. * @param chainId - Chain ID. */ async gasslessMultiTokenApproval(approvalAmount: string, tokenName: string, chainId: number): Promise<TxResponse> { if (!this.vendor.biconomy) { throw new Error(INVALID_BICONOMY_KEY) } const wei = this.vendor.convertToWei(approvalAmount, this.tokenPrecision || 18) const abiEncodedApprove = this.vendor.abiEncodeErc20Functions('approve', [ this.subscriptionPaymentContract?.address, wei, ]) const userAddress = await this.vendor.signer.getAddress() const nonce = await this.getNonceForGaslessERC20(userAddress) const signedMessage = await this.vendor.signedMessageForMultiTokenTx( userAddress, nonce, abiEncodedApprove, this.erc20Contract?.address || '', tokenName, chainId, ) const { r, s, v } = this.vendor.getSignatureParameters(signedMessage) const tx = await this.sendRawBiconomyERC20Transaction(userAddress, abiEncodedApprove, { r, s, v }) return tx } /** * Sends a raw Biconomy ERC-20 transaction. * * @param userAddress - User address. * @param abiEncodedFunction - ABI encoded function. * @param rsv - RSV values. */ async sendRawBiconomyERC20Transaction( userAddress: string, abiEncodedFunction: string, rsv: { r: string; s: string; v: number }, ): Promise<any> { if (this.vendor.biconomy.status === this.vendor.biconomy.READY) { const tx = await this.biconomyERC20Contract?.functions.executeMetaTransaction( userAddress, abiEncodedFunction, rsv.r, rsv.s, rsv.v, ) return tx } else { return new Promise((resolve, reject) => { this.vendor.biconomy .onEvent(this.vendor.biconomy.READY, async () => { const tx = await this.biconomyERC20Contract?.functions.executeMetaTransaction( userAddress, abiEncodedFunction, rsv.r, rsv.s, rsv.v, ) resolve(tx) }) .onEvent(this.vendor.biconomy.ERROR, (error: string) => { reject(error) }) }) } } /** * @remarks * Get nonce for gasless transaction on erc20 * * @param userAddress - user address */ async getNonceForGaslessERC20(userAddress: string): Promise<number> { const nonce = await this.erc20Contract?.functions.getNonce(userAddress) return nonce[0].toNumber() } /** * @remarks * Get user balance of the ERC20 token. * * @param userAddress - user address */ async getUserBalance(userAddress: string): Promise<number> { const wei = await this.erc20Contract?.functions.balanceOf(userAddress) return this.vendor.convertWeiToEth(wei, this.tokenPrecision ?? 18) } /** * @remarks * Get user token balance. * * @param userAddress - user address */ async getUserTokenBalance(userAddress: string): Promise<number> { const userData = await this.subscriptionPaymentContract?.functions.getUserData( userAddress, this.erc20Contract?.address ?? '', ) return this.vendor.convertWeiToEth(userData[0].balance, this.tokenPrecision ?? 18) } /** * @remarks * User deposit to Spheron. * * @param amount - amount to deposit */ async userDeposit(amount: string): Promise<TxResponse> { const wei = this.vendor.convertToWei(amount, this.tokenPrecision ?? 18) return this.subscriptionPaymentContract?.functions.userDeposit(this.erc20Contract?.address ?? '', wei) } async userDepositNative(amount: string): Promise<TxResponse> { const wei = this.vendor.convertToWei(amount, this.tokenPrecision ?? 18) return this.subscriptionNativePaymentContract?.functions.userDeposit(this.erc20Contract?.address ?? '', wei, { value: wei, }) } /** * @remarks * User withdraw from Spheron. * * @param amount - amount to withdraw */ async userWithdraw(amount: string): Promise<TxResponse> { const wei = this.vendor.convertToWei(amount, this.tokenPrecision ?? 18) return this.subscriptionPaymentContract?.functions.userWithdraw(this.erc20Contract?.address ?? '', wei) } /** * @remarks * Get given Allowance amount. * */ async getUsdPricePrecision(): Promise<any> { const bn = await this.subscriptionDataContract?.functions.usdPricePrecision() return bn } /** * @remarks * Get owners list. * */ async getManagers(): Promise<Array<string>> { return await this.subscriptionDataContract?.functions.getManagers() } /** * @remarks * Get govornance address. * */ async getGovernanceAddress(): Promise<string> { return await this.subscriptionDataContract?.functions.governanceAddress() } /** * @remarks * Get escrow address. * */ async getEscrow(): Promise<string> { return await this.subscriptionDataContract?.functions.escrow() } /** * @remarks * Get discount status. * */ async checkIfDiscountsEnabled(): Promise<boolean> { return await this.subscriptionDataContract?.functions.discountsEnabled() } /** * @remarks * Get staking manager address. * */ async getStakingManagerAddress(): Promise<string> { return await this.subscriptionDataContract?.functions.stakingManager() } /** * @remarks * Get staking manager address. * */ async getStakedTokenAddress(): Promise<string> { return await this.subscriptionDataContract?.functions.stakedToken() } /** * @remarks * Get discount slabs. * */ async getDiscountSlabs(): Promise<any> { const slabs = await this.subscriptionDataContract?.functions.discountSlabs() return this.vendor.parseDiscountSlabs(slabs) } /** * @remarks * Get discount slabs. * */ async getDataContract(): Promise<any> { const slabs = await this.subscriptionPaymentContract?.functions.subscriptionData() return this.vendor.parseDiscountSlabs(slabs) } /** * @remarks * this method is used when we want to charge user for the subscrption he will be buying. * @param u - address of user * @param d - array of parameters and their values * @param mf - max gas fee * @param m - max priority fee */ async makeCharge(u: string, d: Array<SubscriptionParameters>, gp?: string): Promise<TxResponse> { const paramArray: Array<string> = [] const paramValue: Array<number> = [] for (let i = 0; i < d.length; i++) { paramArray.push(d[i].param) paramValue.push(this.vendor.convertToBN(d[i].value.toString())) } if (gp) { return await this.subscriptionPaymentContract?.functions.chargeUser( u, paramArray, paramValue, this.erc20Contract?.address || '', { gasPrice: this.vendor.convertToBN(gp), }, ) } else { return await this.subscriptionPaymentContract?.functions.chargeUser( u, paramArray, paramValue, this.erc20Contract?.address || '', ) } } /** * @remarks * this method is used when we want to charge user for the subscrption he will be buying with gas limit * @param u - address of user * @param d - array of parameters and their values * @param gasLimit - gas limit for the tx */ async makeChargeWithGasLimit(u: string, d: Array<SubscriptionParameters>, gasLimit?: number): Promise<TxResponse> { const paramArray: Array<string> = [] const paramValue: Array<number> = [] for (let i = 0; i < d.length; i++) { paramArray.push(d[i].param) paramValue.push(this.vendor.convertToBN(d[i].value.toString())) } if (gasLimit) { return await this.subscriptionPaymentContract?.functions.chargeUser( u, paramArray, paramValue, this.erc20Contract?.address || '', { gasLimit, }, ) } else { return await this.subscriptionPaymentContract?.functions.chargeUser( u, paramArray, paramValue, this.erc20Contract?.address || '', ) } } /** * @remarks * this method is used when we want to charge user for the subscrption he will be buying on mantle or linea chain * @param u - address of user * @param d - array of parameters and their values * @param priceUpdateData - Array of price update data(Pyth Oracle Data) */ async makeChargeMantle(u: string, d: Array<SubscriptionParameters>, priceUpdateData: string[]): Promise<TxResponse> { const paramArray: Array<string> = [] const paramValue: Array<number> = [] for (let i = 0; i < d.length; i++) { paramArray.push(d[i].param) paramValue.push(this.vendor.convertToBN(d[i].value.toString())) } return await this.subscriptionMantlePaymentContract?.functions.chargeUser( u, paramArray, paramValue, this.erc20Contract?.address || '', priceUpdateData, { gasLimit: 300000, }, ) } /** * @remarks * this method is used when we want to charge user for the subscrption he will be buying on mantle or linea chain * @param u - address of user * @param d - array of parameters and their values * @param priceUpdateData - Array of price update data(Pyth Oracle Data) * @param gasLimit - gas limit */ async makeChargeMantleWithGasLimit( u: string, d: Array<SubscriptionParameters>, priceUpdateData: string[], gasLimit: number, ): Promise<TxResponse> { const paramArray: Array<string> = [] const paramValue: Array<number> = [] for (let i = 0; i < d.length; i++) { paramArray.push(d[i].param) paramValue.push(this.vendor.convertToBN(d[i].value.toString())) } return await this.subscriptionMantlePaymentContract?.functions.chargeUser( u, paramArray, paramValue, this.erc20Contract?.address || '', priceUpdateData, { gasLimit, }, ) } /** * @remarks * this method is used to add new tokens to the subscription contract * @param d - array of token data */ async addTokens(d: Array<TokenData>): Promise<TxResponse> { const symbols: Array<string> = [] const tokenAddresses: Array<string> = [] const tokenDecimals: Array<any> = [] const chainLinkBools: Array<boolean> = [] const priceFeedAddresses: Array<string> = [] const priceFeedPrecisions: Array<any> = [] for (let i = 0; i < d.length; i++) { symbols.push(d[i].symobl) tokenAddresses.push(d[i].address) priceFeedAddresses.push(d[i].priceFeedAddress) chainLinkBools.push(d[i].isChainLinkFeed) tokenDecimals.push(this.vendor.convertToBN(d[i].decimals.toString())) priceFeedPrecisions.push(this.vendor.convertToBN(d[i].priceFeedPrecision.toString())) } return await this.subscriptionDataContract?.functions.addNewTokens( symbols, tokenAddresses, tokenDecimals, chainLinkBools, priceFeedAddresses, priceFeedPrecisions, ) } /** * @remarks * this method is used to remove tokens from accepted tokens list * @param d - array of tokens to remove */ async removeTokens(d: Array<string>): Promise<TxResponse> { return await this.subscriptionDataContract?.functions.removeTokens(d) } /** * @remarks * change precision of all the USD values being passed * @param n - prcision number */ async changeUsdPrecision(n: number): Promise<TxResponse> { const bn = this.vendor.convertToBN(n.toString()) return await this.subscriptionDataContract?.functions.changeUsdPrecision(bn) } /** * @remarks * This method is used when we want to update contract parameters * @param params - array of parameters and their values */ async updateParams(params: SubscriptionParameters[]): Promise<TxResponse> { const paramArray: string[] = [] const paramValue: Array<number> = [] for (const param of params) { paramArray.push(param.param) paramValue.push(this.vendor.convertToBN(param.value.toString())) } return await this.subscriptionDataContract?.functions.updateParams(paramArray, paramValue) } /** * @remarks * This method is when we want to delete some parameter * @param paramsToDelete - array of params to delete */ async deleteParams(paramsToDelete: string[]): Promise<TxResponse> { return await this.subscriptionDataContract?.functions.deleteParams(paramsToDelete) } // Admin Functions /** * @remarks * Admin function to get total balances of a particular token */ async getTotalTokenBalance(): Promise<any> { const wei = await this.subscriptionPaymentContract?.functions.getTotalDeposit(this.erc20Contract?.address || '') return this.vendor.convertWeiToEth(wei, this.tokenPrecision ?? 18) } /** * @remarks * Admin function to get the cummulative total of all charges */ async getTotalTokenCharges(): Promise<any> { const wei = await this.subscriptionPaymentContract?.functions.getTotalCharges(this.erc20Contract?.address || '') return this.vendor.convertWeiToEth(wei, this.tokenPrecision ?? 18) } /** * @remarks * Admin function to get the cummulative total of all withdraws made by users */ async getTotalTokenWithdraws(): Promise<any> { const wei = await this.subscriptionPaymentContract?.functions.getTotalWithdraws(this.erc20Contract?.address || '') return this.vendor.convertWeiToEth(wei, this.tokenPrecision ?? 18) } /** * @remarks * Set treasury address to SubscriptionDepay Contract which receives all deposits to the contract * @param treasuryAddress - treasury address */ async setTreasury(treasuryAddress: string): Promise<TxResponse> { return await this.subscriptionPaymentContract?.functions.setTreasury(treasuryAddress) } /** * @remarks * Set company address to SubscriptionDepay Contract which receives company earnings from user charges * Limited to Admin * @param companyAddress - company address */ async setCompany(companyAddress: string): Promise<TxResponse> { return await this.subscriptionPaymentContract?.functions.setCompany(companyAddress) } /** * @remarks * Move company earnings from Treasury to company address * @param amount - amount */ async companyWithdraw(amount: string): Promise<TxResponse> { const wei = this.vendor.convertToWei(amount, this.tokenPrecision ?? 18) return await this.subscriptionPaymentContract?.functions.companyWithdraw(this.erc20Contract?.address || '', wei) } }