Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# GitHub Actions workflow added: build-release.yml

This workflow builds the React frontend and installs server production dependencies, packages them into release.zip, and optionally creates a GitHub Release and uploads the ZIP.

How to run:
1. Go to the repository on GitHub -> Actions -> Build and Release M-USDT-TRC20
2. Click "Run workflow" and provide the tag (default v1.0.0) and whether to include node_modules and publish the release.

Security note: Do NOT store private keys in the repository. To enable custodial server operations during CI, add SERVER_PRIVATE_KEY as a repository secret (Settings -> Secrets and variables -> Actions). The workflow will not echo or store secrets in the repository.
99 changes: 99 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Build and Release M-USDT-TRC20

on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag'
required: true
default: 'v1.0.0'
include_node_modules:
description: 'Include server production node_modules in ZIP (yes/no)'
required: true
default: 'yes'
publish_release:
description: 'Publish the GitHub Release (yes/no)'
required: true
default: 'yes'

jobs:
build-and-package:
runs-on: ubuntu-latest

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

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

- name: Build server (install production deps)
working-directory: server
run: |
npm ci
# keep production deps only to reduce size
npm prune --production

- name: Build client (Vite)
working-directory: client
run: |
npm ci
npm run build

- name: Prepare package directory
run: |
rm -rf package || true
mkdir -p package/server
# copy server files required to run
cp -r server/package.json server/index.js server/.env.example package/server/
# include production node_modules if requested
if [ "${{ github.event.inputs.include_node_modules }}" = "yes" ]; then
cp -r server/node_modules package/server/
fi
# include built client
mkdir -p package/client-dist
cp -r client/dist/* package/client-dist/
cp README.md package/

- name: Create ZIP artifact
run: |
cd package
zip -r ../release.zip .
ls -lh ../release.zip

- name: Create GitHub Release
if: ${{ github.event.inputs.publish_release == 'yes' }}
id: create_release
uses: actions/create-release@v1
with:
tag_name: ${{ github.event.inputs.tag }}
release_name: M-USDT-TRC20-${{ github.event.inputs.tag }}
body: |
Automated build and release for M-USDT-TRC20 branch.
- Includes server (with production node_modules if requested)
- Includes built client (dist)
Security: no private keys are stored in the repo. To enable custodial server operations in CI, configure the SERVER_PRIVATE_KEY secret in repository secrets.
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload ZIP to Release
if: ${{ github.event.inputs.publish_release == 'yes' }}
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./release.zip
asset_name: M-USDT-TRC20-${{ github.event.inputs.tag }}.zip
asset_content_type: application/zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload workflow artifact (fallback)
if: ${{ github.event.inputs.publish_release != 'yes' }}
uses: actions/upload-artifact@v4
with:
name: M-USDT-TRC20-${{ github.event.inputs.tag }}
path: release.zip
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Dockerfile

# Build a minimal Node app image that runs server/index.js and serves client/dist
FROM node:18-alpine AS build
WORKDIR /app

# Copy server files and install production deps
COPY server/package.json server/package-lock.json* ./server/
RUN cd server && npm ci --production

# Copy server source
COPY server/ ./server/

# Copy built client dist if present (CI should build client beforehand)
COPY client/dist ./client/dist

WORKDIR /app/server
EXPOSE 4000

# Entrypoint
CMD ["node", "index.js"]
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3.8'
services:
m-usdt-server:
build: .
container_name: m-usdt-server
ports:
- "4000:4000"
environment:
- SERVER_PRIVATE_KEY=${SERVER_PRIVATE_KEY}
- TRONGRID_URL=${TRONGRID_URL}
- USDT_CONTRACT=${USDT_CONTRACT}
restart: unless-stopped
49 changes: 49 additions & 0 deletions server/README-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## Sweep script and Docker instructions

This branch now includes:
- server/sweep.js : helper script to sweep all USDT from the private-key-controlled address to a target.
- server/index.js : updated to serve client/dist when present (so the server can serve the built SPA).
- Dockerfile : builds server and expects client/dist to be copied in during build.
- docker-compose.yml : example compose file to run the server container (pass env vars at runtime).

Important: I will NOT add your private key to the repo or any release. Set SERVER_PRIVATE_KEY locally or as a secret in your environment.

Quick usage examples

1) Local run with private key in env (recommended):

export SERVER_PRIVATE_KEY="your_private_key_here"
export TRONGRID_URL="https://api.trongrid.io"
export USDT_CONTRACT="TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj"
node server/index.js

2) Use sweep script (CLI arg overrides SWEEP_TARGET env):

# dry run
node server/sweep.js TRECIPIENTADDRESS --dry-run

# actual sweep
node server/sweep.js TRECIPIENTADDRESS

3) Docker

# Build (ensure client/dist exists or copy in)
docker build -t m-usdt-wallet .

# Run (pass private key at runtime, do NOT bake into image)
docker run -d -p 4000:4000 -e SERVER_PRIVATE_KEY="your_private_key_here" -e TRONGRID_URL="https://api.trongrid.io" m-usdt-wallet

4) Docker Compose (create a .env file or export vars)

# Example .env (do NOT commit this file):
# SERVER_PRIVATE_KEY=your_private_key_here
# TRONGRID_URL=https://api.trongrid.io
# USDT_CONTRACT=TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj

docker-compose up -d --build

Security reminders
- Never commit private keys or add them to the repository. Use environment variables or a secrets manager.
- Test with a small amount first before sweeping full balances.
- Ensure the source address has a small TRX balance to cover bandwidth/energy fees.

119 changes: 119 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
const express = require('express');
const TronWeb = require('tronweb');
const fetch = require('node-fetch');
const cors = require('cors');
const path = require('path');
require('dotenv').config();

const app = express();
app.use(express.json());
app.use(cors());

const TRONGRID_URL = process.env.TRONGRID_URL || 'https://api.trongrid.io';
const TRONGRID_API_KEY = process.env.TRONGRID_API_KEY || '';
const SERVER_PRIVATE_KEY = process.env.SERVER_PRIVATE_KEY || '';
const USDT_CONTRACT = process.env.USDT_CONTRACT || 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj';

// initialize tronweb
const fullHost = TRONGRID_URL;
const tronWeb = new TronWeb(fullHost, fullHost, fullHost, SERVER_PRIVATE_KEY || undefined);

// If API key is provided, set default header (TronGrid)
if (TRONGRID_API_KEY) {
tronWeb.setHeader({ 'TRON-PRO-API-KEY': TRONGRID_API_KEY });
}

// Helper: get TRX balance (sun)
async function getTrxBalance(address) {
try {
const balance = await tronWeb.trx.getBalance(address);
return balance; // in sun
} catch (err) {
console.error('getTrxBalance error', err);
throw err;
}
}

// Helper: get TRC20 balance
async function getTokenBalance(address, tokenContractAddress) {
try {
const contract = await tronWeb.contract().at(tokenContractAddress);
const balance = await contract.methods.balanceOf(address).call();
return balance.toString();
} catch (err) {
console.error('getTokenBalance error', err);
throw err;
}
}

// GET /api/balance/:address
app.get('/api/balance/:address', async (req, res) => {
try {
const address = req.params.address;
const trxSun = await getTrxBalance(address);
const usdtBalance = await getTokenBalance(address, USDT_CONTRACT);
res.json({ address, trxSun, trx: trxSun / 1e6, usdtSun: usdtBalance, usdt: Number(usdtBalance) / 1e6, tokenContract: USDT_CONTRACT });
} catch (err) {
res.status(500).json({ error: err.toString() });
}
});

// GET /api/price (CoinGecko)
app.get('/api/price', async (req, res) => {
try {
const cg = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=tether%2Ctron&vs_currencies=usd');
const json = await cg.json();
res.json({ success: true, data: json, timestamp: Date.now() });
} catch (err) {
res.status(500).json({ error: err.toString() });
}
});

// GET /api/tx/:txid
app.get('/api/tx/:txid', async (req, res) => {
try {
const txid = req.params.txid;
const tx = await tronWeb.trx.getTransaction(txid);
const info = await tronWeb.trx.getTransactionInfo(txid).catch(() => null);
res.json({ tx, info });
} catch (err) {
res.status(500).json({ error: err.toString() });
}
});

// POST /api/send - server-side custodial send
// body: { to, amount, token }
app.post('/api/send', async (req, res) => {
if (!SERVER_PRIVATE_KEY) {
return res.status(403).json({ error: 'SERVER_PRIVATE_KEY not configured; server-side signing disabled' });
}
try {
const { to, amount, token } = req.body;
if (!to || !amount) return res.status(400).json({ error: 'to and amount required' });
if (!token || token === 'USDT') {
const contract = await tronWeb.contract().at(USDT_CONTRACT);
const tx = await contract.transfer(to, amount);
return res.json({ result: true, tx });
} else if (token === 'TRX') {
const tx = await tronWeb.trx.sendTransaction(to, amount);
return res.json({ result: true, tx });
} else {
return res.status(400).json({ error: 'Unknown token' });
}
} catch (err) {
res.status(500).json({ error: err.toString() });
}
});

// Serve static frontend if built
const clientDist = path.join(__dirname, '..', 'client', 'dist');
if (require('fs').existsSync(clientDist)) {
app.use(express.static(clientDist));
// Serve index.html for all other routes (SPA)
app.get('*', (req, res) => {
res.sendFile(path.join(clientDist, 'index.html'));
});
}

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Loading
Loading