UNPKG

@goequitize/rwa-token-sdk

Version:

SDK for creating and managing RWA token transactions with compliance features

703 lines (702 loc) 29 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.EvmRwaToken = void 0; const ethers_1 = require("ethers"); const RwaTokenBase_1 = require("../base/RwaTokenBase"); const RWATokenABIJson = __importStar(require("../utils/abi/RWATokenABI.json")); const WhitelistABIJson = __importStar(require("../utils/abi/Whitelist.json")); // Use type assertion to access the ABI // eslint-disable-next-line @typescript-eslint/no-explicit-any const RWATokenABI = RWATokenABIJson; const tokenAbi = RWATokenABI.abi; // Use type assertion to access the ABI // eslint-disable-next-line @typescript-eslint/no-explicit-any const WhitelistABI = WhitelistABIJson; const whitelistAbi = WhitelistABI.abi; /** * Default EVM chain configurations */ const defaultChainConfigs = { // Ethereum Mainnet 1: { explorerUrl: 'https://etherscan.io/tx/${txHash}', chainName: 'Ethereum Mainnet', gasModel: 'eip1559' }, // Ethereum Testnet - Goerli 5: { explorerUrl: 'https://goerli.etherscan.io/tx/${txHash}', chainName: 'Ethereum Goerli Testnet', gasModel: 'eip1559' }, // Ethereum Testnet - Sepolia 11155111: { explorerUrl: 'https://sepolia.etherscan.io/tx/${txHash}', chainName: 'Ethereum Sepolia Testnet', gasModel: 'eip1559' }, // Binance Smart Chain Mainnet 56: { explorerUrl: 'https://bscscan.com/tx/${txHash}', chainName: 'BSC Mainnet', gasModel: 'legacy' }, // Binance Smart Chain Testnet 97: { explorerUrl: 'https://testnet.bscscan.com/tx/${txHash}', chainName: 'BSC Testnet', gasModel: 'legacy' }, // Polygon Mainnet 137: { explorerUrl: 'https://polygonscan.com/tx/${txHash}', chainName: 'Polygon Mainnet', gasModel: 'eip1559' }, // Polygon Mumbai Testnet 80001: { explorerUrl: 'https://mumbai.polygonscan.com/tx/${txHash}', chainName: 'Polygon Mumbai Testnet', gasModel: 'eip1559' } }; /** * EVM-compatible implementation of RWA Token operations * Works across any EVM-compatible chain */ class EvmRwaToken extends RwaTokenBase_1.RwaTokenBase { /** * Create a new instance of the EVM RWA Token module * @param provider The JSON-RPC provider instance * @param chainId The chain ID of the connected network * @param tokenAddress The address of the RWA token contract * @param customChainConfig Optional custom chain configuration */ constructor(provider, chainId, tokenAddress, customChainConfig) { super(provider, chainId); this.tokenAddress = tokenAddress; // Use default chain config if available, otherwise create a generic one const defaultConfig = defaultChainConfigs[chainId] || { explorerUrl: '', chainName: `Chain ID ${chainId}`, gasModel: 'legacy', }; // Merge custom config with default this.chainConfig = { ...defaultConfig, ...customChainConfig, functionNames: { forceTransfer: 'forceTransfer', isWhitelisted: 'isWhitelisted', paused: 'paused', ...(defaultConfig.functionNames || {}), ...(customChainConfig?.functionNames || {}) } }; } /** * Get the explorer URL for a transaction * @param txHash The transaction hash * @returns The explorer URL or empty string if not available */ getExplorerUrl(txHash) { if (!this.chainConfig.explorerUrl) return ''; return this.chainConfig.explorerUrl.replace('${txHash}', txHash); } /** * Validate if an address is whitelisted for token transfers * @param address The address to check * @returns True if the address is whitelisted, false otherwise */ async isWhitelisted(address) { try { // Get the whitelist contract address from the token contract const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); const whitelistAddress = await tokenContract.whitelist(); if (!whitelistAddress || whitelistAddress === ethers_1.ethers.ZeroAddress) { throw new Error('Whitelist contract address not set'); } // Use the configured function name or default const whitelistFunctionName = this.chainConfig.functionNames?.isWhitelisted || 'isWhitelisted'; // Create the whitelist contract instance const whitelistContract = new ethers_1.ethers.Contract(whitelistAddress, [...whitelistAbi, `function ${whitelistFunctionName}(address account) view returns (bool)`], this.provider); return await whitelistContract[whitelistFunctionName](address); } catch (error) { console.error(`Failed to check whitelist status: ${error instanceof Error ? error.message : String(error)}`); return false; // Assume not whitelisted on error } } /** * Check if the token is currently paused * @returns True if the token is paused, false otherwise */ async isPaused() { try { const pausedFunctionName = this.chainConfig.functionNames?.paused || 'paused'; const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, [...tokenAbi, `function ${pausedFunctionName}() view returns (bool)`], this.provider); return await tokenContract[pausedFunctionName](); } catch (error) { console.error(`Failed to check pause status: ${error instanceof Error ? error.message : String(error)}`); return false; // Assume not paused on error } } /** * Check if an address has a specific role * @param address The address to check * @param role The role to check for (e.g., 'MINTER_ROLE', 'BURNER_ROLE', 'ADMIN_ROLE') * @returns True if the address has the role, false otherwise */ async hasRole(address, role) { try { // Convert role string to bytes32 keccak256 hash if it's not already a hash let roleHash; if (role.startsWith('0x') && role.length === 66) { roleHash = role; } else { roleHash = ethers_1.ethers.keccak256(ethers_1.ethers.toUtf8Bytes(role)); } const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, [...tokenAbi, 'function hasRole(bytes32 role, address account) view returns (bool)'], this.provider); return await tokenContract.hasRole(roleHash, address); } catch (error) { console.error(`Failed to check role: ${error instanceof Error ? error.message : String(error)}`); return false; // Assume no role on error } } /** * Create an unsigned transaction with appropriate gas settings based on chain * @param to Destination address * @param data Transaction data * @param value Native token value (optional) * @param gasLimit Gas limit (optional) * @param gasInfo Gas price information * @returns Unsigned transaction object */ createUnsignedTransaction(to, data, value = '0', gasLimit, gasInfo) { const rawTx = { to, data, value }; if (gasLimit) { rawTx.gasLimit = gasLimit.toString(); } // Add appropriate gas price fields based on the chain's gas model if (this.chainConfig.gasModel === 'eip1559') { rawTx.maxFeePerGas = gasInfo.recommended.maxFeePerGas; rawTx.maxPriorityFeePerGas = gasInfo.recommended.maxPriorityFeePerGas; } else { // Legacy gas model rawTx.maxFeePerGas = gasInfo.recommended.gasPrice; } return rawTx; } /** * Calculate gas cost for transaction display * @param gasLimit Gas limit * @param gasInfo Gas price information * @returns Formatted gas cost */ calculateGasCost(gasLimit, gasInfo) { let gasCost; if (this.chainConfig.gasModel === 'eip1559') { gasCost = gasLimit * BigInt(ethers_1.ethers.parseUnits(gasInfo.recommended.maxFeePerGas, 'gwei')); } else { // Legacy gas model gasCost = gasLimit * BigInt(ethers_1.ethers.parseUnits(gasInfo.recommended.gasPrice, 'gwei')); } return ethers_1.ethers.formatEther(gasCost); } /** * Build a mint transaction * @param params The parameters for the mint operation * @returns The transaction build result */ async buildMintTxn(params) { try { // Check if the sender has the MINTER_ROLE const isMinter = await this.hasRole(params.from, 'MINTER_ROLE'); if (!isMinter) { return { isError: true, errorMsg: 'Sender does not have MINTER_ROLE', }; } // Check if recipient is whitelisted const isRecipientWhitelisted = await this.isWhitelisted(params.to); if (!isRecipientWhitelisted) { return { isError: true, errorMsg: 'Recipient is not whitelisted', }; } // Get token decimals const decimals = await this.getDecimals(this.tokenAddress); // Convert human-readable amount to token units const amountInTokenUnits = ethers_1.ethers.parseUnits(params.amount, decimals); // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Encode the mint function call const data = tokenContract.interface.encodeFunctionData('mint', [ params.to, amountInTokenUnits ]); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.from, to: this.tokenAddress, data }); // Get gas price information const gasInfo = await this.getGasInfo('mint'); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build mint transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build a burn transaction * @param params The parameters for the burn operation * @returns The transaction build result */ async buildBurnTxn(params) { try { // Check if the sender has the BURNER_ROLE const isBurner = await this.hasRole(params.sender, 'BURNER_ROLE'); if (!isBurner) { return { isError: true, errorMsg: 'Sender does not have BURNER_ROLE', }; } // Get token decimals const decimals = await this.getDecimals(this.tokenAddress); // Convert human-readable amount to token units const amountInTokenUnits = ethers_1.ethers.parseUnits(params.amount, decimals); // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Encode the burn function call const data = tokenContract.interface.encodeFunctionData('burn', [ params.from, amountInTokenUnits ]); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.sender, to: this.tokenAddress, data }); // Get gas price information const gasInfo = await this.getGasInfo('burn'); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build burn transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build a transfer transaction * @param params The parameters for the transfer operation * @returns The transaction build result */ async buildTransferTxn(params) { try { // Check if token is paused const isPaused = await this.isPaused(); if (isPaused) { return { isError: true, errorMsg: 'Token transfers are paused', }; } // Check if recipient is whitelisted const isRecipientWhitelisted = await this.isWhitelisted(params.to); if (!isRecipientWhitelisted) { return { isError: true, errorMsg: 'Recipient is not whitelisted', }; } // Get token decimals const decimals = await this.getDecimals(this.tokenAddress); // Convert human-readable amount to token units const amountInTokenUnits = ethers_1.ethers.parseUnits(params.amount, decimals); // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Encode the transfer function call const data = tokenContract.interface.encodeFunctionData('transfer', [ params.to, amountInTokenUnits ]); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.from, to: this.tokenAddress, data }); // Get gas price information const gasInfo = await this.getGasInfo('transfer'); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build transfer transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build a forced transfer transaction (admin override) * @param params The parameters for the forced transfer operation * @returns The transaction build result */ async buildForcedTransferTxn(params) { try { // Check if the sender has the ADMIN_ROLE const isAdmin = await this.hasRole(params.from, 'ADMIN_ROLE'); if (!isAdmin) { return { isError: true, errorMsg: 'Sender does not have ADMIN_ROLE', }; } // Get token decimals const decimals = await this.getDecimals(this.tokenAddress); // Convert human-readable amount to token units const amountInTokenUnits = ethers_1.ethers.parseUnits(params.amount, decimals); // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Use the configured force transfer function name or default const forceTransferFunctionName = this.chainConfig.functionNames?.forceTransfer || 'forceTransfer'; // Encode the forceTransfer function call const data = tokenContract.interface.encodeFunctionData(forceTransferFunctionName, [ params.from, params.to, amountInTokenUnits ]); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.from, to: this.tokenAddress, data }); // Get gas price information const gasInfo = await this.getGasInfo('transfer'); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build forced transfer transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build a pause transaction * @param params Parameters for the pause operation * @returns The transaction build result */ async buildPauseTxn(params) { try { // Check if the sender has the ADMIN_ROLE const isAdmin = await this.hasRole(params.from, 'ADMIN_ROLE'); if (!isAdmin) { return { isError: true, errorMsg: 'Sender does not have ADMIN_ROLE', }; } // Check if token is already paused const isPaused = await this.isPaused(); if (isPaused) { return { isError: true, errorMsg: 'Token is already paused', }; } // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Encode the pause function call const data = tokenContract.interface.encodeFunctionData('pause', []); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.from, to: this.tokenAddress, data }); // Get gas price information const gasInfo = await this.getGasInfo(); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build pause transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build an unpause transaction * @param params Parameters for the unpause operation * @returns The transaction build result */ async buildUnpauseTxn(params) { try { // Check if the sender has the ADMIN_ROLE const isAdmin = await this.hasRole(params.from, 'ADMIN_ROLE'); if (!isAdmin) { return { isError: true, errorMsg: 'Sender does not have ADMIN_ROLE', }; } // Check if token is already unpaused const isPaused = await this.isPaused(); if (!isPaused) { return { isError: true, errorMsg: 'Token is already unpaused', }; } // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Encode the unpause function call const data = tokenContract.interface.encodeFunctionData('unpause', []); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.from, to: this.tokenAddress, data }); // Get gas price information const gasInfo = await this.getGasInfo(); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build unpause transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Build a cross-chain transfer transaction using LayerZero's OFT standard * @param params The parameters for the cross-chain transfer operation * @returns The transaction build result */ async buildCrossChainTransferTxn(params) { try { // Check if token is paused const isPaused = await this.isPaused(); if (isPaused) { return { isError: true, errorMsg: 'Token transfers are paused', }; } // Check if recipient is whitelisted const isRecipientWhitelisted = await this.isWhitelisted(params.to); if (!isRecipientWhitelisted) { return { isError: true, errorMsg: 'Recipient is not whitelisted', }; } // Get token decimals const decimals = await this.getDecimals(this.tokenAddress); // Convert human-readable amount to token units const amountInTokenUnits = ethers_1.ethers.parseUnits(params.amount, decimals); // Create the token contract instance const tokenContract = new ethers_1.ethers.Contract(this.tokenAddress, tokenAbi, this.provider); // Default adapter params if not provided // This is the standard adapter params for a simple cross-chain transfer const adapterParams = params.adapterParams || ethers_1.ethers.solidityPacked(['uint16', 'uint256'], [1, 200000] // version 1, 200k gas limit ); // Encode the sendFrom function call for OFT // The sendFrom function is part of the OFT standard const data = tokenContract.interface.encodeFunctionData('sendFrom', [ params.to, // from address params.destinationChainId, // destination chain ID params.destinationAddress, // destination address amountInTokenUnits, // amount params.to, // refund address (same as from) ethers_1.ethers.ZeroAddress, // zroPaymentAddress (not used) adapterParams // adapter parameters ]); // Estimate gas limit const gasLimit = await this.provider.estimateGas({ from: params.from, to: this.tokenAddress, data }); // Get gas price information // Cross-chain transfers typically need higher gas const gasInfo = await this.getGasInfo('transfer'); // Create the unsigned transaction const rawTx = this.createUnsignedTransaction(this.tokenAddress, data, '0', gasLimit, gasInfo); // Calculate approximate gas cost const gasCost = this.calculateGasCost(gasLimit, gasInfo); return { isError: false, data: { rawTx, gasCost, gasPrice: this.chainConfig.gasModel === 'eip1559' ? gasInfo.recommended.maxFeePerGas : gasInfo.recommended.gasPrice, gasLimit: gasLimit.toString() } }; } catch (error) { return { isError: true, errorMsg: `Failed to build cross-chain transfer transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Override the broadcast method to include chain-specific explorer URLs * @param signedTxn The signed transaction hex string * @returns Transaction response information */ async broadcast(signedTxn) { const response = await super.broadcast(signedTxn); // Add chain-specific explorer URL if (response.hash) { response.explorerURL = this.getExplorerUrl(response.hash); } return response; } } exports.EvmRwaToken = EvmRwaToken;