Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/add trc20 support #118

Merged
merged 7 commits into from
Jan 26, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { LateInitConnectedWallet } from '../../../lib/wallets/LateInitConnectedW
import { useSettings } from '../../../context/Settings';
import { Jetton } from '../../../lib/wallets/Jetton';
import { ERC20 } from '../../../lib/wallets/ERC20';
import { TRC20 } from '../../../lib/wallets/TRC20';

const logger = getLogger(LOGGER_NAME_RELAY);

Expand Down Expand Up @@ -178,6 +179,11 @@ export const CreateTransaction = ({ asset, inboundRelayParams, setSignTxResponse
(derivation as ERC20).setToAddress(toAddress);
(derivation as ERC20).setNativeAsset(asset.nativeAsset);
}
if (asset.address && asset.protocol === 'TRX') {
(derivation as TRC20).setTokenAddress(asset.address);
(derivation as TRC20).setDecimals(asset.decimals);
(derivation as ERC20).setToAddress(toAddress);
Comment on lines +182 to +185
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can optimize these multiple ifs, but it's ok for now

}
if (rpcUrl !== null) derivation!.setRPCUrl(rpcUrl); // this must remain the last method called on derivation for ERC20 support

return await derivation!.prepare?.(toAddress, values.memo);
Expand Down
5 changes: 5 additions & 0 deletions apps/recovery-relay/lib/wallets/ERC20/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export class ERC20 extends EVMBase implements ConnectedWallet {

public setDecimals(decimals: number) {
this.decimals = decimals;
if (!this.decimals) {
this.relayLogger.error(`ERC20 Token decimals are unavailable: ${this.assetId}`);
throw new Error(`ERC20 Token decimals are unavailable: ${this.assetId}`);
}

this.normalizingFactor = (10 ** decimals).toString();
}

Expand Down
7 changes: 6 additions & 1 deletion apps/recovery-relay/lib/wallets/Jetton/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,15 @@ export class Jetton extends BaseTon implements LateInitConnectedWallet {
throw new Error(`TON Jettons: wallet's contract address unavailable`);
}

if (!this.decimals) {
this.relayLogger.error(`TON Jettons: token decimals not set`);
throw new Error(`TON Jettons: token decimals not set`);
}

await new Promise((resolve) => setTimeout(resolve, 2000));

const { stack } = await this.client.runMethod(contractAddress, 'get_wallet_data');
const normalizingFactor = 10 ** this.decimals!;
const normalizingFactor = 10 ** this.decimals;

return stack.readNumber() / normalizingFactor;
} else {
Expand Down
137 changes: 137 additions & 0 deletions apps/recovery-relay/lib/wallets/TRC20/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Tron as BaseTron, Input } from '@fireblocks/wallet-derivation';
import { ConnectedWallet } from '../ConnectedWallet';
import { abi } from './trc20.abi';
import { AccountData } from '../types';
import { WalletClasses } from '..';
import { Tron } from '../TRON';
import zlib from 'node:zlib';
import { promisify } from 'util';

export class TRC20 extends BaseTron implements ConnectedWallet {
constructor(private input: Input) {
super(input);
}

protected backendWallet: Tron | undefined;

public rpcURL: string | undefined;

private decimals: number | undefined;

private tokenAddress: string | undefined;

private toAddress: string | undefined;

private tronWeb: any | undefined;

public setDecimals(decimals: number): void {
this.decimals = decimals;
}

public setTokenAddress(tokenAddress: string): void {
this.tokenAddress = tokenAddress;
}

public setToAddress(toAddress: string) {
this.toAddress = toAddress;
}

public setRPCUrl(url: string): void {
this.rpcURL = url;
const TronWeb = require('tronweb');
const { HttpProvider } = TronWeb.providers;
const endpointUrl = this.rpcURL;
const fullNode = new HttpProvider(endpointUrl);
const solidityNode = new HttpProvider(endpointUrl);
const eventServer = new HttpProvider(endpointUrl);
//random prvKey is used for gas estimations
const randomPrvKey = Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
this.tronWeb = new TronWeb(fullNode, solidityNode, eventServer, randomPrvKey);
}

public async getBalance(): Promise<number> {
const contract = await this.tronWeb.contract(abi, this.tokenAddress);
return (await contract.balanceOf(this.address).call()).toNumber();
}

public async prepare(): Promise<AccountData> {
if (!this.decimals) {
this.relayLogger.error('TRC20: Decimals not set');
throw new Error('TRC20: Decimals not set');
}
const balance = ((await this.getBalance()) / 10 ** this.decimals) as number;
const trxBalance = await this.getTrxBalance();

const extraParams = new Map<string, any>();

extraParams.set('t', this.tokenAddress);
extraParams.set('d', this.decimals);
extraParams.set('r', this.rpcURL);

const feeRate = (await this.estimateGas()) ?? 40_000_000;

const preparedData: AccountData = {
balance,
feeRate,
extraParams,
insufficientBalance: balance <= 0,
insufficientBalanceForTokenTransfer: trxBalance < feeRate,
};

this.relayLogger.logPreparedData('TRC20', preparedData);
return preparedData;
}

public async broadcastTx(tx: string): Promise<string> {
try {
// decompress and decode
const gunzip = promisify(zlib.gunzip);
const compressedBuffer = Buffer.from(tx, 'base64');
const decompressedBuffer = await gunzip(new Uint8Array(compressedBuffer));
const signedTx = JSON.parse(decompressedBuffer.toString());

//broadcast the tx
const result = await this.tronWeb.trx.sendRawTransaction(signedTx);
if ('code' in result) {
this.relayLogger.error(`TRC20: Error broadcasting tx: ${JSON.stringify(result, null, 2)}`);
throw new Error(result.code);
}
this.relayLogger.debug(`TRC20: Tx broadcasted: ${result.txid}`);
return result.txid;
} catch (e) {
this.relayLogger.error('TRC20: Error broadcasting tx:', e);
throw new Error(`TRC20: Error broadcasting tx: ${e}`);
}
}

private async getTrxBalance(): Promise<number> {
return await this.tronWeb.trx.getBalance(this.address);
}

private async estimateGas(): Promise<number | undefined> {
try {
const functionSelector = 'transfer(address,uint256)';
const balance = await this.getBalance();
const parameter = [
{ type: 'address', value: this.toAddress },
{ type: 'uint256', value: balance },
];
// Trigger a dry-run of the transaction to estimate energy consumption
const energyEstimate = await this.tronWeb.transactionBuilder.triggerConstantContract(
this.tokenAddress,
functionSelector,
parameter,
);

// Get current energy prices from network
const parameterInfo = await this.tronWeb.trx.getChainParameters();
const energyFeeParameter = parameterInfo.find((param: any) => param.key === 'getEnergyFee');
const energyFee = energyFeeParameter ? energyFeeParameter.value : 420;

return energyEstimate.energy * energyFee * 1.1; // add 10% margin
} catch (error) {
this.relayLogger.error(`TRC20: Error estimating gas, ${error}`);
return undefined;
}
}
}
20 changes: 20 additions & 0 deletions apps/recovery-relay/lib/wallets/TRC20/trc20.abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const abi = [
{
outputs: [{ type: 'uint256' }],
constant: true,
inputs: [{ name: 'who', type: 'address' }],
name: 'balanceOf',
stateMutability: 'View',
type: 'Function',
},
{
outputs: [{ type: 'bool' }],
inputs: [
{ name: '_to', type: 'address' },
{ name: '_value', type: 'uint256' },
],
name: 'transfer',
stateMutability: 'Nonpayable',
type: 'Function',
},
];
21 changes: 18 additions & 3 deletions apps/recovery-relay/lib/wallets/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getAllJettons, getAllERC20s } from '@fireblocks/asset-config';
import { getAllJettons, getAllERC20s, getAllTRC20s } from '@fireblocks/asset-config';
import { Cardano } from './ADA';
import { Cosmos } from './ATOM';
import { Bitcoin, BitcoinCash, BitcoinSV, DASH, DogeCoin, LiteCoin, ZCash } from './BTCBased';
Expand Down Expand Up @@ -40,6 +40,7 @@ import { CoreDAO } from './EVM/CORE_COREDAO';
import { Ton } from './TON';
import { Jetton } from './Jetton';
import { ERC20 } from './ERC20';
import { TRC20 } from './TRC20';
export { ConnectedWallet } from './ConnectedWallet';

const fillJettons = () => {
Expand All @@ -56,8 +57,8 @@ const fillJettons = () => {
};

const fillERC20s = () => {
const jerc20List = getAllERC20s();
const erc20Tokens = jerc20List.reduce(
const erc20List = getAllERC20s();
const erc20Tokens = erc20List.reduce(
(prev, curr) => ({
...prev,
[curr]: ERC20,
Expand All @@ -68,6 +69,19 @@ const fillERC20s = () => {
return erc20Tokens;
};

const fillTRC20s = () => {
const trc20List = getAllTRC20s();
const erc20Tokens = trc20List.reduce(
(prev, curr) => ({
...prev,
[curr]: TRC20,
}),
{},
) as any;
Object.keys(erc20Tokens).forEach((key) => (erc20Tokens[key] === undefined ? delete erc20Tokens[key] : {}));
return erc20Tokens;
};

export const WalletClasses = {
ALGO: Algorand,
ALGO_TEST: Algorand,
Expand Down Expand Up @@ -152,6 +166,7 @@ export const WalletClasses = {
TON_TEST: Ton,
...fillJettons(),
...fillERC20s(),
...fillTRC20s(),
} as const;

type WalletClass = (typeof WalletClasses)[keyof typeof WalletClasses];
Expand Down
52 changes: 52 additions & 0 deletions apps/recovery-utility/renderer/lib/wallets/TRC20/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable spaced-comment */
/* eslint-disable import/order */
import { Tron as BaseTron } from '@fireblocks/wallet-derivation';
import { SigningWallet } from '../SigningWallet';
import { GenerateTxInput, TxPayload } from '../types';
import zlib from 'zlib';
import { promisify } from 'util';

export class TRC20 extends BaseTron implements SigningWallet {
public async generateTx({ to, amount, feeRate, extraParams }: GenerateTxInput): Promise<TxPayload> {
try {
const rpcUrl = extraParams?.get('r');

// eslint-disable-next-line global-require
const TronWeb = require('tronweb');
const { HttpProvider } = TronWeb.providers;
const fullNode = new HttpProvider(rpcUrl);
const solidityNode = new HttpProvider(rpcUrl);
const eventServer = new HttpProvider(rpcUrl);
const tronWeb = new TronWeb(fullNode, solidityNode, eventServer, this.privateKey?.replace('0x', ''));

const decimals = extraParams?.get('d');
const tokenAddress = extraParams?.get('t');

const functionSelector = 'transfer(address,uint256)';
const parameter = [
{ type: 'address', value: to },
{ type: 'uint256', value: amount * 10 ** decimals },
];

const tx = await tronWeb.transactionBuilder.triggerSmartContract(
tokenAddress,
functionSelector,
{ feeLimit: feeRate },
parameter,
);

const signedTx = await tronWeb.trx.sign(tx.transaction);

this.utilityLogger.logSigningTx('TRC20', signedTx);

//encode and compress for qr code
const gzip = promisify(zlib.gzip);
const compressedTx = (await gzip(JSON.stringify(signedTx))).toString('base64');

return { tx: compressedTx };
} catch (e) {
this.utilityLogger.error('TRC20: Error generating tx:', e);
throw new Error(`TRC20: Error generating tx: ${e}`);
}
}
}
13 changes: 12 additions & 1 deletion apps/recovery-utility/renderer/lib/wallets/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// import { Bitcoin } from './BTC';
import { assets, getAllJettons } from '@fireblocks/asset-config';
import { assets, getAllJettons, getAllTRC20s } from '@fireblocks/asset-config';
import { ETC } from '@fireblocks/wallet-derivation';
import { Ripple } from './XRP';
import { Cosmos } from './ATOM';
Expand All @@ -23,6 +23,7 @@ import { Celestia } from './CELESTIA';
import { Ton } from './TON';
import { Jetton } from './Jetton';
import { ERC20 } from './ERC20';
import { TRC20 } from './TRC20';

const fillEVMs = () => {
const evms = Object.keys(assets).reduce(
Expand Down Expand Up @@ -50,6 +51,15 @@ const fillJettons = () => {
return jettons;
};

const fillTRC20s = () => {
const trc20List = getAllTRC20s();
const trc20s: { [key: string]: any } = {};
for (const trc20 of trc20List) {
trc20s[trc20] = TRC20;
}
return trc20s;
};

export { SigningWallet as BaseWallet } from './SigningWallet';

export const WalletClasses = {
Expand Down Expand Up @@ -91,6 +101,7 @@ export const WalletClasses = {
CELESTIA: Celestia,
CELESTIA_TEST: Celestia,
...fillEVMs(),
...fillTRC20s(),

// EDDSA
SOL: Solana,
Expand Down
7 changes: 6 additions & 1 deletion packages/asset-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,15 @@ To add support for withdrawals of a listed ERC20 on supported EVM chain, make su
The token contract address must be present in the `globalAssets` list as the `address` parameter.
Note: the tool will support ERC20 token withdrawals only on EVM chains that has withdrawal support for the base asset as well.

### Add a new TRC20 token

To add support for withdrawals of a listed TRC20 token, make sure the token is listed in `globalAssets.ts`.
The token contract address must be present in the `globalAssets` list as the `address` parameter, make sure that `decimals` is also updated accordingly.

### Add a new Jetton token

To add support for withdrawals of a listed Jetton, make sure the token is listed in `globalAssets.ts`.
The Jetton master contract address must be present in the `globalAssets` list as the `address` parameter.
The Jetton master contract address must be present in the `globalAssets` list as the `address` parameter, make sure that `decimals` is also updated accordingly.

### Token or new Base Asset Support

Expand Down
10 changes: 10 additions & 0 deletions packages/asset-config/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,13 @@ export function getAllERC20s(): string[] {
}
return erc20Tokens;
}

export function getAllTRC20s(): string[] {
const trc20s = [];
for (const asset of globalAssets) {
if (asset.protocol === 'TRX' && asset.address) {
trc20s.push(asset.id);
}
}
return trc20s;
}
Loading