@goequitize/rwa-token-sdk
Version:
SDK for creating and managing RWA token transactions with compliance features
703 lines (702 loc) • 29 kB
JavaScript
"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;