Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/tron-smart-contracts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Tron Smart Contracts Tests

on:
pull_request:
branches:
- master
paths:
- 'packages/smart-contracts/tron/**'
- 'packages/smart-contracts/tronbox-config.js'
- '.github/workflows/tron-smart-contracts.yml'
push:
branches:
- master
paths:
- 'packages/smart-contracts/tron/**'
- 'packages/smart-contracts/tronbox-config.js'
workflow_dispatch:

jobs:
tron-compile-check:
name: Tron Contract Compilation Check
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'yarn'

- name: Install TronBox globally
run: npm install -g tronbox

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Compile Tron contracts
working-directory: packages/smart-contracts
run: yarn tron:compile

- name: Verify build artifacts exist
working-directory: packages/smart-contracts
run: |
echo "Checking build artifacts..."
ls -la tron-build/

# Verify key contracts were compiled
if [ ! -f "tron-build/ERC20FeeProxy.json" ]; then
echo "ERROR: ERC20FeeProxy.json not found!"
exit 1
fi

if [ ! -f "tron-build/TestTRC20.json" ]; then
echo "ERROR: TestTRC20.json not found!"
exit 1
fi

echo "✅ All required artifacts present"

- name: Verify contract ABI structure
working-directory: packages/smart-contracts
run: |
echo "Verifying ERC20FeeProxy ABI..."
# Check that the compiled contract has the expected function
if ! grep -q "transferFromWithReferenceAndFee" tron-build/ERC20FeeProxy.json; then
echo "ERROR: ERC20FeeProxy missing transferFromWithReferenceAndFee function!"
exit 1
fi
echo "✅ Contract ABI structure verified"

# Note: Integration tests are skipped in CI due to QEMU ARM64 emulation being too slow
# for Tron blockchain confirmations. Run integration tests locally with:
# docker run -d --name tron-tre -p 9090:9090 tronbox/tre
# yarn tron:test
# Or run against Nile testnet:
# TRON_PRIVATE_KEY=your_key yarn tron:test:nile
10 changes: 10 additions & 0 deletions packages/currency/src/chains/declarative/data/nile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const chainId = 'nile';

// Nile is Tron's test network
export const testnet = true;

// Test tokens on Nile testnet
// Note: These are testnet token addresses, not mainnet
export const currencies = {
// Add testnet token addresses as needed
};
19 changes: 19 additions & 0 deletions packages/currency/src/chains/declarative/data/tron.ts
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
export const chainId = 'tron';

// Tron mainnet configuration
export const testnet = false;

// Common TRC20 tokens on Tron
export const currencies = {
// USDT-TRC20 - the most widely used stablecoin on Tron
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t: {
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
},
// USDC on Tron
TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8: {
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
},
};
2 changes: 2 additions & 0 deletions packages/currency/src/chains/declarative/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CurrencyTypes } from '@requestnetwork/types';

import * as TronDefinition from './data/tron';
import * as NileDefinition from './data/nile';
import * as SolanaDefinition from './data/solana';
import * as StarknetDefinition from './data/starknet';
import * as TonDefinition from './data/ton';
Expand All @@ -11,6 +12,7 @@ export type DeclarativeChain = CurrencyTypes.Chain;

export const chains: Record<CurrencyTypes.DeclarativeChainName, DeclarativeChain> = {
tron: TronDefinition,
nile: NileDefinition,
solana: SolanaDefinition,
starknet: StarknetDefinition,
ton: TonDefinition,
Expand Down
3 changes: 2 additions & 1 deletion packages/currency/src/chains/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import BtcChains from './btc/BtcChains';
import EvmChains from './evm/EvmChains';
import NearChains from './near/NearChains';
import TronChains from './tron/TronChains';
import DeclarativeChains from './declarative/DeclarativeChains';
import { isSameChain } from './utils';

export { BtcChains, EvmChains, NearChains, DeclarativeChains, isSameChain };
export { BtcChains, EvmChains, NearChains, TronChains, DeclarativeChains, isSameChain };
6 changes: 6 additions & 0 deletions packages/currency/src/chains/tron/TronChains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ChainsAbstract } from '../ChainsAbstract';
import { CurrencyTypes, RequestLogicTypes } from '@requestnetwork/types';
import { TronChain, chains } from './index';

class TronChains extends ChainsAbstract<CurrencyTypes.TronChainName, TronChain, string> {}
export default new TronChains(chains, RequestLogicTypes.CURRENCY.ETH);
11 changes: 11 additions & 0 deletions packages/currency/src/chains/tron/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CurrencyTypes } from '@requestnetwork/types';

import * as TronDefinition from '../declarative/data/tron';
import * as NileDefinition from '../declarative/data/nile';

export type TronChain = CurrencyTypes.Chain;

export const chains: Record<CurrencyTypes.TronChainName, TronChain> = {
tron: TronDefinition,
nile: NileDefinition,
};
2 changes: 2 additions & 0 deletions packages/payment-processor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export * from './payment/encoder-approval';
export * as Escrow from './payment/erc20-escrow-payment';
export * from './payment/prepared-transaction';
export * from './payment/utils-near';
export * from './payment/utils-tron';
export * from './payment/tron-fee-proxy';
export * from './payment/single-request-forwarder';
export * from './payment/erc20-recurring-payment-proxy';
export * from './payment/erc20-commerce-escrow-wrapper';
Expand Down
191 changes: 191 additions & 0 deletions packages/payment-processor/src/payment/tron-fee-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { BigNumber, BigNumberish } from 'ethers';
import { ClientTypes, ExtensionTypes } from '@requestnetwork/types';
import { TronChains } from '@requestnetwork/currency';

import { getAmountToPay, getRequestPaymentValues, validateRequest } from './utils';
import {
TronWeb,
ITronTransactionCallback,
processTronFeeProxyPayment,
approveTrc20,
getTronAllowance,
isTronAccountSolvent,
isValidTronAddress,
getERC20FeeProxyAddress,
} from './utils-tron';
import { validatePaymentReference } from '../utils/validation';

/**
* Checks if the TronWeb instance has sufficient allowance for the payment
*/
export async function hasSufficientTronAllowance(
request: ClientTypes.IRequestData,
tronWeb: TronWeb,
amount?: BigNumberish,
): Promise<boolean> {
const network = request.currencyInfo.network;
if (!network || !TronChains.isChainSupported(network)) {
throw new Error('Request currency network is not a supported Tron network');
}
TronChains.assertChainSupported(network);

const tokenAddress = request.currencyInfo.value;
const { feeAmount } = getRequestPaymentValues(request);
const amountToPay = getAmountToPay(request, amount);
const totalAmount = BigNumber.from(amountToPay).add(feeAmount || 0);

const allowance = await getTronAllowance(tronWeb, tokenAddress, network);
return allowance.gte(totalAmount);
}

/**
* Checks if the payer has sufficient TRC20 token balance
*/
export async function hasSufficientTronBalance(
request: ClientTypes.IRequestData,
tronWeb: TronWeb,
amount?: BigNumberish,
): Promise<boolean> {
const tokenAddress = request.currencyInfo.value;
const { feeAmount } = getRequestPaymentValues(request);
const amountToPay = getAmountToPay(request, amount);
const totalAmount = BigNumber.from(amountToPay).add(feeAmount || 0);

return isTronAccountSolvent(tronWeb, tokenAddress, totalAmount);
}

/**
* Approves the ERC20FeeProxy contract to spend TRC20 tokens for a request payment
*/
export async function approveTronFeeProxyRequest(
request: ClientTypes.IRequestData,
tronWeb: TronWeb,
amount?: BigNumberish,
callback?: ITronTransactionCallback,
): Promise<string> {
const network = request.currencyInfo.network;
if (!network || !TronChains.isChainSupported(network)) {
throw new Error('Request currency network is not a supported Tron network');
}
TronChains.assertChainSupported(network);

validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT);

const tokenAddress = request.currencyInfo.value;
const { feeAmount } = getRequestPaymentValues(request);
const amountToPay = getAmountToPay(request, amount);
const totalAmount = BigNumber.from(amountToPay).add(feeAmount || 0);

return approveTrc20(tronWeb, tokenAddress, network, totalAmount, callback);
}

/**
* Processes a TRC20 fee proxy payment for a Request.
*
* @param request The request to pay
* @param tronWeb The TronWeb instance connected to the payer's wallet
* @param amount Optionally, the amount to pay. Defaults to remaining amount of the request.
* @param feeAmount Optionally, the fee amount to pay. Defaults to the fee amount from the request.
* @param callback Optional callbacks for transaction events
* @returns The transaction hash
*/
export async function payTronFeeProxyRequest(
request: ClientTypes.IRequestData,
tronWeb: TronWeb,
amount?: BigNumberish,
feeAmount?: BigNumberish,
callback?: ITronTransactionCallback,
): Promise<string> {
const network = request.currencyInfo.network;
if (!network || !TronChains.isChainSupported(network)) {
throw new Error('Request currency network is not a supported Tron network');
}
TronChains.assertChainSupported(network);

validateRequest(request, ExtensionTypes.PAYMENT_NETWORK_ID.ERC20_FEE_PROXY_CONTRACT);

const {
paymentReference,
paymentAddress,
feeAddress,
feeAmount: requestFeeAmount,
} = getRequestPaymentValues(request);

validatePaymentReference(paymentReference);

if (!isValidTronAddress(paymentAddress)) {
throw new Error(`Invalid Tron payment address: ${paymentAddress}`);
}

const tokenAddress = request.currencyInfo.value;
const amountToPay = getAmountToPay(request, amount);
const feeToPay = feeAmount ?? requestFeeAmount ?? '0';

// Check allowance
const totalAmount = BigNumber.from(amountToPay).add(feeToPay);
const allowance = await getTronAllowance(tronWeb, tokenAddress, network);

if (allowance.lt(totalAmount)) {
throw new Error(
`Insufficient TRC20 allowance. Required: ${totalAmount.toString()}, Available: ${allowance.toString()}. ` +
`Please call approveTronFeeProxyRequest first.`,
);
}

// Check balance
const hasSufficientBalance = await isTronAccountSolvent(tronWeb, tokenAddress, totalAmount);
if (!hasSufficientBalance) {
throw new Error('Insufficient TRC20 token balance for payment');
}

return processTronFeeProxyPayment(
tronWeb,
network,
tokenAddress,
paymentAddress,
amountToPay,
paymentReference,
feeToPay,
feeAddress || tronWeb.defaultAddress.base58,
callback,
);
}

/**
* Gets information needed to pay a Tron request
*/
export function getTronPaymentInfo(
request: ClientTypes.IRequestData,
amount?: BigNumberish,
): {
proxyAddress: string;
tokenAddress: string;
paymentAddress: string;
amount: string;
paymentReference: string;
feeAmount: string;
feeAddress: string;
} {
const network = request.currencyInfo.network;
if (!network || !TronChains.isChainSupported(network)) {
throw new Error('Request currency network is not a supported Tron network');
}
TronChains.assertChainSupported(network);

const { paymentReference, paymentAddress, feeAddress, feeAmount } =
getRequestPaymentValues(request);

const tokenAddress = request.currencyInfo.value;
const amountToPay = getAmountToPay(request, amount);
const proxyAddress = getERC20FeeProxyAddress(network);

return {
proxyAddress,
tokenAddress,
paymentAddress,
amount: amountToPay.toString(),
paymentReference: paymentReference ?? '',
feeAmount: (feeAmount || '0').toString(),
feeAddress: feeAddress ?? '',
};
}
Loading