@nitin-spheron/desub-js
Version:
Typescript library for working with the decentralised subscription contracts in Spheron
727 lines (686 loc) • 23.8 kB
text/typescript
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)
}
}