UNPKG

@everstake/wallet-sdk-polygon

Version:
507 lines (444 loc) 15.8 kB
/** * Copyright (c) 2025, Everstake. * Licensed under the BSD-3-Clause License. See LICENSE file for details. */ import { Blockchain } from '../../utils'; import { CheckToken, SetStats } from '../../utils/api'; import { COMMON_ERROR_MESSAGES } from '../../utils/constants/errors'; import Web3, { Contract, HttpProvider, Numbers } from 'web3'; import { ERROR_MESSAGES, ORIGINAL_ERROR_MESSAGES } from './constants/errors'; import { ABI_CONTRACT_APPROVE, ABI_CONTRACT_BUY, ABI_CONTRACT_STAKING, } from './abi'; import { ADDRESS_CONTRACT_APPROVE, ADDRESS_CONTRACT_APPROVE_POL, ADDRESS_CONTRACT_BUY, ADDRESS_CONTRACT_STAKING, CHAIN, CLAIM_REWARDS_BASE_GAS, CLAIM_UNDELEGATE_BASE_GAS, DELEGATE_BASE_GAS, MIN_AMOUNT, RESTAKE_BASE_GAS, RPC_URL, UNDELEGATE_BASE_GAS, WITHDRAW_EPOCH_DELAY, } from './constants'; import BigNumber from 'bignumber.js'; import { TransactionRequest, UnbondInfo } from './types'; /** * The `Polygon` class extends the `Blockchain` class and provides methods for interacting with the Polygon network. * * It handles initialization of Web3 and multiple contract instances, including approval contracts, * buy contracts, and staking contracts. It also manages error messages related to contract operations. * * @property {Web3} web3 - The Web3 instance used for interacting with the Polygon network. * @property {Contract} contract_approve - The contract instance for token approval. * @property {Contract} contract_approve_pol - The contract instance for POL token approval. * @property {Contract} contract_buy - The contract instance for token purchase logic. * @property {Contract} contract_staking - The contract instance for staking logic. * @property ERROR_MESSAGES - The standardized error messages for the Polygon class. * @property ORIGINAL_ERROR_MESSAGES - The raw/original error messages for internal mapping or debugging. * * @constructor * Creates an instance of the `Polygon` class. * @param {string} [rpc=RPC_URL] - The RPC URL of the Polygon network. */ export class Polygon extends Blockchain { public contract_approve!: Contract<typeof ABI_CONTRACT_APPROVE>; public contract_approve_pol!: Contract<typeof ABI_CONTRACT_APPROVE>; public contract_buy: Contract<typeof ABI_CONTRACT_BUY>; public contract_staking: Contract<typeof ABI_CONTRACT_BUY>; private web3!: Web3; protected ERROR_MESSAGES = ERROR_MESSAGES; protected ORIGINAL_ERROR_MESSAGES = ORIGINAL_ERROR_MESSAGES; constructor(rpc: string = RPC_URL) { super(); const httpProvider = new HttpProvider(rpc); this.web3 = new Web3(httpProvider); this.contract_approve = new this.web3.eth.Contract( ABI_CONTRACT_APPROVE, ADDRESS_CONTRACT_APPROVE, ); this.contract_approve_pol = new this.web3.eth.Contract( ABI_CONTRACT_APPROVE, ADDRESS_CONTRACT_APPROVE_POL, ); this.contract_buy = new this.web3.eth.Contract( ABI_CONTRACT_BUY, ADDRESS_CONTRACT_BUY, ); this.contract_staking = new this.web3.eth.Contract( ABI_CONTRACT_STAKING, ADDRESS_CONTRACT_STAKING, ); } /** * Checks if a transaction is still pending or has been confirmed. * * @param {string} hash - The transaction hash to check. * @returns {Promise<{ result: boolean }>} * * @throws {Error} Throws an error with code `'TRANSACTION_LOADING_ERR'` if an issue occurs while fetching the transaction status. * */ public async isTransactionLoading( hash: string, ): Promise<{ result: boolean }> { try { const result = await this.web3.eth.getTransactionReceipt(hash); if (result && result.status) { return { result: false }; } else { await this.isTransactionLoading(hash); return { result: true }; } } catch (error) { throw this.handleError('TRANSACTION_LOADING_ERR', error); } } /** approve returns TX loading status * @param {string} address - user's address * @param {string} amount - amount for approve * @param {boolean} isPOL - is POL token (false - old MATIC) * @returns {Promise<Object>} Promise object the result of boolean type */ public async approve( address: string, amount: string, isPOL = false, ): Promise< | { from: string; to: string | undefined; gasLimit: bigint; data: string; } | undefined > { const amountWei = await this.web3.utils.toWei(amount.toString(), 'ether'); if (new BigNumber(amountWei).isLessThan(MIN_AMOUNT)) { throw new Error( `Min Amount ${this.web3.utils.fromWei(MIN_AMOUNT.toString(), 'ether').toString()} matic`, ); } const contract = isPOL ? this.contract_approve_pol : this.contract_approve; if (!contract?.methods?.approve) return; try { const gasEstimate = await contract.methods .approve(ADDRESS_CONTRACT_STAKING, amountWei) .estimateGas({ from: address }); // Create the transaction return { from: address, to: contract.options.address, gasLimit: gasEstimate, data: contract.methods .approve(ADDRESS_CONTRACT_STAKING, amountWei) .encodeABI(), }; } catch (error) { throw this.handleError('APPROVE_ERR', error); } } /** delegate makes unsigned delegation TX * @param {string} token - auth token * @param {string} address - user's address * @param {string} amount - amount for approve * @param {boolean} isPOL - is POL token (false - old MATIC) * @returns {Promise<Object>} Promise object represents the unsigned TX object */ public async delegate( token: string, address: string, amount: string, isPOL = false, ): Promise<TransactionRequest | undefined> { if (await CheckToken(token)) { const amountWei = await this.web3.utils.toWei(amount.toString(), 'ether'); if (new BigNumber(amountWei).isLessThan(MIN_AMOUNT)) throw new Error(`Min Amount ${MIN_AMOUNT} wei matic`); try { const allowedAmount = await this.getAllowance(address, isPOL); if ( allowedAmount && new BigNumber(allowedAmount.toString()).isLessThan(amountWei) ) { this.throwError('ALLOWANCE_ERR'); } const methods = this.contract_buy?.methods; if (!methods?.buyVoucherPOL || !methods?.buyVoucher) return; const method = isPOL ? methods.buyVoucherPOL(amountWei, 0) : methods.buyVoucher(amountWei, 0); // Create the transaction const tx = { from: address, to: ADDRESS_CONTRACT_BUY, gasLimit: DELEGATE_BASE_GAS, data: method.encodeABI(), }; await SetStats({ token, action: 'stake', amount: Number(amount), address, chain: CHAIN, }); // Sign the transaction return tx; } catch (error) { throw this.handleError('DELEGATE_ERR', error); } } else { throw new Error(COMMON_ERROR_MESSAGES.TOKEN_ERROR); } } /** undelegate makes unsigned undelegate TX * @param {string} token - auth token * @param {string} address - user's address * @param {string} amount - amount for approve * @param {boolean} isPOL - is POL token (false - old MATIC) * @returns {Promise<Object>} Promise object represents the unsigned TX object */ public async undelegate( token: string, address: string, amount: string, isPOL = false, ): Promise<TransactionRequest | undefined> { if (await CheckToken(token)) { try { const amountWei = await this.web3.utils.toWei( amount.toString(), 'ether', ); const delegatedBalance = await this.getTotalDelegate(address); if ( delegatedBalance && delegatedBalance.isLessThan(BigNumber(amount)) ) { this.throwError('DELEGATED_BALANCE_ERR'); } const methods = this.contract_buy.methods; if (!methods.sellVoucher_newPOL || !methods.sellVoucher_new) return; const method = isPOL ? methods.sellVoucher_newPOL(amountWei, amountWei) : methods.sellVoucher_new(amountWei, amountWei); // Create the transaction const tx = { from: address, to: ADDRESS_CONTRACT_BUY, gasLimit: UNDELEGATE_BASE_GAS, data: method.encodeABI(), }; await SetStats({ token, action: 'unstake', amount: Number(amount), address, chain: CHAIN, }); return tx; } catch (error) { throw this.handleError('UNDELEGATE_ERR', error); } } else { throw new Error(COMMON_ERROR_MESSAGES.TOKEN_ERROR); } } /** claimUndelegate makes unsigned claim undelegate TX * @param {string} address - user's address * @param {bigint} unbondNonce - unbound nonce * @param {boolean} isPOL - is POL token (false - old MATIC) * @returns {Promise<Object>} Promise object represents the unsigned TX object */ public async claimUndelegate( address: string, unbondNonce = 0n, isPOL = false, ): Promise<TransactionRequest | undefined> { const unbond = await this.getUnbond(address, unbondNonce); if (unbond == null) return; if (BigNumber(unbond.amount).isZero()) throw new Error(`Nothing to claim`); const currentEpoch = await this.getCurrentEpoch(); if (currentEpoch == null) return; if ( BigNumber(currentEpoch.toString()).isLessThan( BigNumber(unbond.withdrawEpoch.toString()).plus( BigNumber(WITHDRAW_EPOCH_DELAY.toString()), ), ) ) { throw new Error(`Current epoch less than withdraw delay`); } const methods = this.contract_buy.methods; if (!methods.unstakeClaimTokens_newPOL || !methods.unstakeClaimTokens_new) return; const method = isPOL ? methods.unstakeClaimTokens_newPOL(unbond.unbondNonces) : methods.unstakeClaimTokens_new(unbond.unbondNonces); return { from: address, to: ADDRESS_CONTRACT_BUY, gasLimit: CLAIM_UNDELEGATE_BASE_GAS, data: method.encodeABI(), }; } /** reward makes unsigned claim reward TX * @param {string} address - user's address * @param {boolean} isPOL - is POL token (false - old MATIC) * @returns {Promise<Object>} Promise object represents the unsigned TX object */ public async reward( address: string, isPOL = false, ): Promise<TransactionRequest | undefined> { const methods = this.contract_buy.methods; if (!methods.withdrawRewardsPOL || !methods.withdrawRewards) return; const method = isPOL ? methods.withdrawRewardsPOL() : methods.withdrawRewards(); // Create the transaction return { from: address, to: ADDRESS_CONTRACT_BUY, gasLimit: CLAIM_REWARDS_BASE_GAS, data: method.encodeABI(), }; } /** restake makes unsigned restake reward TX * @param {string} address - user's address * @param {boolean} isPOL - is POL token (false - old MATIC) * @returns {Promise<Object>} Promise object represents the unsigned TX object */ public async restake( address: string, isPOL = false, ): Promise<TransactionRequest | undefined> { const methods = this.contract_buy.methods; if (!methods.restakePOL || !methods.restake) return; const method = isPOL ? methods.restakePOL() : methods.restake(); // Create the transaction return { from: address, to: ADDRESS_CONTRACT_BUY, gasLimit: RESTAKE_BASE_GAS, data: method.encodeABI(), }; } /** getReward returns reward number * @param {string} address - user's address * @returns {Promise<BigNumber>} Promise with number of the reward */ public async getReward(address: string): Promise<BigNumber | undefined> { try { const methods = this.contract_buy.methods; if (!methods.getLiquidRewards) return; const result = await methods.getLiquidRewards(address).call(); if (!this.isNumbers(result)) return; return new BigNumber(this.web3.utils.fromWei(result, 'ether')); } catch (error) { throw this.handleError('GET_REWARD_ERR', error); } } /** getAllowance returns allowed number for spender * @param {string} owner - tokens owner * @param {boolean} isPOL - is POL token (false - old MATIC) * @param {string} spender - contract spender * @returns {Promise<bigint>} Promise allowed bigint for spender */ public async getAllowance( owner: string, isPOL = false, spender = ADDRESS_CONTRACT_STAKING, ): Promise<bigint | undefined> { const contract = isPOL ? this.contract_approve_pol : this.contract_approve; if (!contract.methods.allowance) return; try { return await contract.methods.allowance(owner, spender).call(); } catch (error) { throw this.handleError('GET_ALLOWANCE_ERR', error); } } /** getTotalDelegate returns total delegated number * @param {string} address - user's address * @returns {Promise<BigNumber>} Promise with BigNumber of the delegation */ public async getTotalDelegate( address: string, ): Promise<BigNumber | undefined> { try { const methods = this.contract_buy.methods; if (!methods.getTotalStake) return; const result = await methods.getTotalStake(address).call(); const res = result?.[0]; if (res == null) return; return new BigNumber(this.web3.utils.fromWei(res, 'ether')); } catch (error) { throw this.handleError('GET_TOTAL_DELEGATE_ERR', error); } } /** getUnbond returns unbound data * @param {string} address - user's address * @param {bigint} unbondNonce - unbound nonce * @returns {Promise<Object>} Promise Object with unbound data */ public async getUnbond( address: string, unbondNonce = 0n, ): Promise<UnbondInfo | undefined> { try { const methods = this.contract_buy.methods; if (!methods.unbondNonces || !methods.unbonds_new) return; const unbondNoncesRes: bigint = await methods .unbondNonces(address) .call(); // Get recent nonces if not provided const unbondNonces = unbondNonce === 0n ? unbondNoncesRes : unbondNonce; const result = await methods.unbonds_new(address, unbondNonces).call(); const res0 = result?.[0]; const res1 = result?.[1]; if (res0 == null || res1 == null || typeof unbondNonce !== 'bigint') return; return { amount: new BigNumber(this.web3.utils.fromWei(res0, 'ether')), withdrawEpoch: res1, unbondNonces, }; } catch (error) { throw this.handleError('GET_UNBOND_ERR', error); } } /** getUnbondNonces returns unbound nonce * @param {string} address - user's address * @returns {Promise<bigint>} Promise with unbound nonce bigint */ public async getUnbondNonces(address: string): Promise<bigint | undefined> { try { const methods = this.contract_buy.methods; if (!methods.unbondNonces) return; return await methods.unbondNonces(address).call(); } catch (error) { throw this.handleError('GET_UNBOND_NONCE_ERR', error); } } /** getCurrentEpoch returns current epoch * @returns {Promise<bigint>} Promise with current epoch bigint */ public async getCurrentEpoch(): Promise<bigint | undefined> { const methods = this.contract_staking.methods; if (!methods.currentEpoch) return; return await methods.currentEpoch().call(); } private isNumbers(value: unknown): value is Numbers { return ( typeof value === 'number' || typeof value === 'bigint' || typeof value === 'string' ); } }