@mapprotocol/common-contracts 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/utils/index.ts CHANGED
@@ -1,25 +1,42 @@
1
+ /**
2
+ * @mapprotocol/common-contracts/utils
3
+ *
4
+ * Shared TypeScript utilities for MAP Protocol contract operations.
5
+ *
6
+ * Quick start:
7
+ * const deployer = createDeployer(hre, { autoVerify: true });
8
+ * await deployer.deploy("MyContract", [arg1], "optional_salt");
9
+ * await deployer.deployProxy("MyContract", [initArg], "optional_salt");
10
+ * await deployer.upgrade("MyContract", proxyAddr);
11
+ */
12
+
13
+ // Unified deployer — recommended entry point for deploy/upgrade/verify
14
+ export {
15
+ createDeployer,
16
+ type Deployer,
17
+ type DeployResult,
18
+ type DeployProxyResult,
19
+ type DeployerOptions
20
+ } from "./deployer";
21
+
22
+ // Deploy record — deploy.json address management
1
23
  export {
2
- resolveDeploymentEnv,
24
+ resolveDeploymentPath,
3
25
  getDeploymentByKey,
4
26
  hasDeployment,
5
27
  saveDeployment,
28
+ type DeploymentPath,
6
29
  type DeploymentOptions
7
- } from "./deployment";
30
+ } from "./deployRecord";
8
31
 
9
- export {
10
- tronDeploy,
11
- getTronContract,
12
- getTronDeployer,
13
- tronFromHex,
14
- tronToHex,
15
- isTronNetwork,
16
- createTronWeb,
17
- type TronConfig
18
- } from "./tronHelper";
32
+ // Contract verification
33
+ export { verify, type VerifyOptions } from "./verifier";
19
34
 
20
- export {
21
- addressToHex,
22
- isBase58,
23
- isTronAddress,
24
- isSolanaChain
25
- } from "./addressCodec";
35
+ // Code hash for CREATE2 address prediction
36
+ export { getCodeHash, getProxyCodeHash, getCustomProxyCodeHash } from "./codeHash";
37
+
38
+ // Address encoding
39
+ export { addressToHex, isBase58, isTronAddress, isSolanaChain } from "./addressCodec";
40
+
41
+ // Tron utilities
42
+ export { TronClient, tronFromHex, tronToHex, isTronNetwork, sendAndWait, waitForTx, type TronAddress } from "./tronHelper";
@@ -4,77 +4,359 @@ let TronWeb = require("tronweb");
4
4
  // Pure address conversion — no RPC, no private key needed
5
5
  // ============================================================
6
6
 
7
+ /** Convert hex address to Tron base58 format. Returns as-is if already base58. */
7
8
  export function tronFromHex(hex: string): string {
8
9
  if (hex.startsWith("T") && hex.length === 34) return hex;
9
10
  return TronWeb.address.fromHex(hex);
10
11
  }
11
12
 
13
+ /** Convert Tron base58 address to 0x-prefixed hex. Returns as-is if already hex. */
12
14
  export function tronToHex(addr: string): string {
13
15
  if (addr.startsWith("0x") && addr.length === 42) return addr;
14
16
  return TronWeb.address.toHex(addr).replace(/^(41)/, "0x");
15
17
  }
16
18
 
17
- export function isTronNetwork(network: string): boolean {
18
- return network === "Tron" || network === "tron_test" || network === "Tron_test";
19
+ // Tron chainIds: mainnet 728126428, nile testnet 3448148188
20
+ const TRON_CHAIN_IDS = [728126428, 3448148188];
21
+
22
+ /** Check if a network name or chainId belongs to Tron (case-insensitive). */
23
+ export function isTronNetwork(networkOrChainId: string | number): boolean {
24
+ if (typeof networkOrChainId === "number") {
25
+ return TRON_CHAIN_IDS.includes(networkOrChainId);
26
+ }
27
+ return networkOrChainId.toLowerCase().startsWith("tron");
28
+ }
29
+
30
+ export interface TronAddress {
31
+ hex: string; // 0x-prefixed address
32
+ base58: string; // T-prefixed address
19
33
  }
20
34
 
21
35
  // ============================================================
22
- // TronWeb instance creation
36
+ // TronClient
23
37
  // ============================================================
24
38
 
25
- export interface TronConfig {
26
- rpcUrl: string;
27
- privateKey?: string; // optional — omit for read-only operations
28
- }
39
+ /**
40
+ * TronClient — encapsulates tronWeb instance, provides high-level contract operations.
41
+ *
42
+ * Usage:
43
+ * // From hardhat runtime (recommended in tasks)
44
+ * let client = TronClient.fromHre(hre);
45
+ *
46
+ * // Manual — with rpc and private key
47
+ * let client = new TronClient("https://api.trongrid.io", "your_private_key");
48
+ *
49
+ * // Read-only — no private key
50
+ * let client = new TronClient("https://api.trongrid.io");
51
+ *
52
+ * // Contract interaction
53
+ * let gw = await client.getContract(artifacts, "Gateway", addr);
54
+ * let wtoken = await gw.wToken().call(); // read
55
+ * await gw.setWtoken(client.toHex(wtoken)).sendAndWait(); // write + wait
56
+ *
57
+ * // Deploy
58
+ * let { hex, base58 } = await client.deploy(artifacts, "Gateway");
59
+ * let { proxy, implementation } = await client.deployProxy(artifacts, "Gateway", [admin]);
60
+ *
61
+ * // Upgrade
62
+ * await client.upgradeProxy(artifacts, "Gateway", proxyAddr);
63
+ */
64
+ export class TronClient {
65
+ private tronWeb: any;
66
+ private connected: boolean = false;
67
+
68
+ /**
69
+ * Create a TronClient with explicit rpcUrl.
70
+ * @param rpcUrl - Tron full host URL (e.g. "https://api.trongrid.io")
71
+ * @param privateKey - optional, omit for read-only operations
72
+ */
73
+ constructor(rpcUrl: string, privateKey?: string) {
74
+ const opts: any = { fullHost: rpcUrl };
75
+ if (privateKey) {
76
+ // TronWeb doesn't accept 0x prefix on private keys
77
+ opts.privateKey = privateKey.startsWith("0x") ? privateKey.slice(2) : privateKey;
78
+ }
79
+ this.tronWeb = new TronWeb(opts);
80
+ }
81
+
82
+ /**
83
+ * Create TronClient from hardhat runtime environment.
84
+ * Reads rpcUrl and privateKey from hre.network.config. Verifies RPC connectivity.
85
+ */
86
+ static fromHre(hre: any): TronClient {
87
+ const config = hre.network.config;
88
+ let rpcUrl: string = config.url;
89
+ let privateKey = Array.isArray(config.accounts) ? config.accounts[0] : undefined;
90
+ if (!rpcUrl) throw new Error(`no rpc url configured for network ${hre.network.name}`);
91
+ // Hardhat config uses JSON-RPC path (e.g. /jsonrpc), TronWeb needs base URL
92
+ rpcUrl = rpcUrl.replace(/\/(jsonrpc|wallet|solidity)\/?$/i, "");
93
+ // TronWeb doesn't accept 0x prefix on private keys
94
+ if (privateKey && privateKey.startsWith("0x")) {
95
+ privateKey = privateKey.slice(2);
96
+ }
97
+ return new TronClient(rpcUrl, privateKey);
98
+ }
99
+
100
+ /** Verify RPC is reachable. Uses getNowBlock which is widely supported. */
101
+ private async _checkConnection(): Promise<void> {
102
+ if (this.connected) return;
103
+ try {
104
+ // getNowBlock calls /wallet/getnowblock — more universally supported than getNodeInfo
105
+ await this.tronWeb.trx.getCurrentBlock();
106
+ this.connected = true;
107
+ } catch (e: any) {
108
+ const url = this.tronWeb.fullNode?.host || "unknown";
109
+ throw new Error(
110
+ `TronClient: cannot connect to ${url}. ` +
111
+ `Ensure the URL is a valid TronWeb full host (e.g. "https://api.trongrid.io") and is reachable. ` +
112
+ `Error: ${e.message || e}`
113
+ );
114
+ }
115
+ }
116
+
117
+ /** Lazy connection check — called before first contract operation. */
118
+ private async _ensureConnected(): Promise<void> {
119
+ if (!this.connected) await this._checkConnection();
120
+ }
121
+
122
+ /** Get the default operator address in base58 format. */
123
+ get defaultAddress(): string {
124
+ return this.tronWeb.defaultAddress.base58;
125
+ }
126
+
127
+ /** Convert Tron base58 address to 0x hex. */
128
+ toHex(addr: string): string {
129
+ return tronToHex(addr);
130
+ }
131
+
132
+ /** Convert 0x hex to Tron base58 address. */
133
+ fromHex(hex: string): string {
134
+ return tronFromHex(hex);
135
+ }
136
+
137
+ /**
138
+ * Get a contract instance. Write methods have .sendAndWait() attached.
139
+ * @param artifacts - hardhat artifacts (hre.artifacts)
140
+ * @param contractName - contract name (e.g. "Gateway")
141
+ * @param addr - contract address (base58 or hex)
142
+ */
143
+ async getContract(artifacts: any, contractName: string, addr: string): Promise<any> {
144
+ await this._ensureConnected();
145
+ console.log("operator address is:", this.defaultAddress);
146
+ const artifact = await artifacts.readArtifact(contractName);
147
+ const contract = await this.tronWeb.contract(artifact.abi, addr);
148
+ return this._wrapContract(contract);
149
+ }
150
+
151
+ /**
152
+ * Deploy contract. If salt is provided, uses CREATE2 factory.
153
+ * @param artifacts - hardhat artifacts
154
+ * @param contractName - contract name to deploy
155
+ * @param args - constructor arguments
156
+ * @param salt - optional CREATE2 salt for deterministic address
157
+ * @param feeLimit - Tron fee limit (default 15 TRX)
158
+ */
159
+ async deploy(
160
+ artifacts: any,
161
+ contractName: string,
162
+ args: any[] = [],
163
+ salt: string = "",
164
+ feeLimit: number = 15_000_000_000
165
+ ): Promise<TronAddress> {
166
+ await this._ensureConnected();
167
+ return tronDeploy(this.tronWeb, artifacts, contractName, args, salt, feeLimit);
168
+ }
169
+
170
+ /**
171
+ * Deploy implementation + ERC1967 proxy. If salt is provided, proxy uses factory.
172
+ * @param artifacts - hardhat artifacts
173
+ * @param contractName - implementation contract name
174
+ * @param initArgs - initialize() function arguments
175
+ * @param salt - optional CREATE2 salt for proxy
176
+ * @param feeLimit - Tron fee limit
177
+ */
178
+ async deployProxy(
179
+ artifacts: any,
180
+ contractName: string,
181
+ initArgs: any[] = [],
182
+ salt: string = "",
183
+ feeLimit: number = 15_000_000_000
184
+ ): Promise<{ proxy: TronAddress; implementation: TronAddress }> {
185
+ await this._ensureConnected();
186
+ return tronDeployProxy(this.tronWeb, artifacts, contractName, initArgs, salt, feeLimit);
187
+ }
188
+
189
+ /**
190
+ * Upgrade a UUPS proxy to a new implementation.
191
+ * @param artifacts - hardhat artifacts
192
+ * @param contractName - new implementation contract name
193
+ * @param proxyAddr - proxy address to upgrade
194
+ * @param feeLimit - Tron fee limit
195
+ */
196
+ async upgradeProxy(
197
+ artifacts: any,
198
+ contractName: string,
199
+ proxyAddr: string,
200
+ feeLimit: number = 15_000_000_000
201
+ ): Promise<TronAddress> {
202
+ await this._ensureConnected();
203
+ return tronUpgradeProxy(this.tronWeb, artifacts, contractName, proxyAddr, feeLimit);
204
+ }
205
+
206
+ /**
207
+ * Wait for a transaction to be confirmed on-chain.
208
+ * @param txId - transaction hash from .send()
209
+ */
210
+ async waitForTx(txId: string, retries: number = 20, interval: number = 3000): Promise<any> {
211
+ return waitForTx(this.tronWeb, txId, retries, interval);
212
+ }
213
+
214
+ /** Wrap contract methods — adds .sendAndWait() to write methods. */
215
+ private _wrapContract(contract: any): any {
216
+ const tronWeb = this.tronWeb;
217
+ return new Proxy(contract, {
218
+ get(target, prop) {
219
+ const original = target[prop];
220
+ if (typeof original !== "function") return original;
29
221
 
30
- export function createTronWeb(config: TronConfig) {
31
- const opts: any = { fullHost: config.rpcUrl };
32
- if (config.privateKey) {
33
- opts.privateKey = config.privateKey;
222
+ return (...args: any[]) => {
223
+ const methodCall = original.apply(target, args);
224
+ if (methodCall && typeof methodCall.send === "function") {
225
+ methodCall.sendAndWait = (opts?: Record<string, any>) =>
226
+ sendAndWait(methodCall, tronWeb, opts);
227
+ }
228
+ return methodCall;
229
+ };
230
+ }
231
+ });
34
232
  }
35
- return new TronWeb(opts);
36
233
  }
37
234
 
38
235
  // ============================================================
39
- // Contract interaction requires tronWeb instance
236
+ // Low-level functions (used internally by TronClient)
40
237
  // ============================================================
41
238
 
239
+ /** @internal Deploy contract on Tron. If salt is provided, uses CREATE2 factory. */
42
240
  export async function tronDeploy(
43
241
  tronWeb: any,
44
242
  artifacts: any,
45
243
  contractName: string,
46
244
  args: any[] = [],
245
+ salt: string = "",
47
246
  feeLimit: number = 15_000_000_000
48
- ): Promise<string> {
49
- let c = await artifacts.readArtifact(contractName);
247
+ ): Promise<TronAddress> {
248
+ if (salt) {
249
+ const { tronDeployByFactory } = require("./factory");
250
+ const hex: string = await tronDeployByFactory(tronWeb, artifacts, contractName, salt, args, feeLimit);
251
+ return { hex, base58: tronFromHex(hex) };
252
+ }
253
+
254
+ const artifact = await artifacts.readArtifact(contractName);
50
255
  console.log("deploy address is:", tronWeb.defaultAddress.base58);
51
- let instance = await tronWeb.contract().new({
52
- abi: c.abi,
53
- bytecode: c.bytecode,
256
+ const instance = await tronWeb.contract().new({
257
+ abi: artifact.abi,
258
+ bytecode: artifact.bytecode,
54
259
  feeLimit,
55
260
  callValue: 0,
56
261
  parameters: args,
57
262
  });
58
- let hex = instance.address; // 41-prefixed hex
59
- let base58 = tronWeb.address.fromHex(hex);
263
+ const rawAddress = instance.address; // 41-prefixed hex
264
+ const hex = rawAddress.replace(/^41/, "0x");
265
+ const base58 = tronWeb.address.fromHex(rawAddress);
60
266
  console.log(`${contractName} deployed: ${base58} (${hex})`);
61
- return hex.replace(/^41/, "0x");
267
+ return { hex, base58 };
268
+ }
269
+
270
+ /** @internal Deploy implementation + ERC1967 proxy on Tron. */
271
+ export async function tronDeployProxy(
272
+ tronWeb: any,
273
+ artifacts: any,
274
+ contractName: string,
275
+ initArgs: any[] = [],
276
+ salt: string = "",
277
+ feeLimit: number = 15_000_000_000
278
+ ): Promise<{ proxy: TronAddress; implementation: TronAddress }> {
279
+ const implementation = await tronDeploy(tronWeb, artifacts, contractName, [], "", feeLimit);
280
+ console.log(`${contractName} implementation: ${implementation.base58} (${implementation.hex})`);
281
+
282
+ // Wait for implementation to be fully confirmed before deploying proxy
283
+ if (salt) {
284
+ console.log("waiting for implementation to be indexed...");
285
+ await new Promise(r => setTimeout(r, 5000));
286
+ }
287
+
288
+ const artifact = await artifacts.readArtifact(contractName);
289
+ const iface = new (require("ethers").Interface)(artifact.abi);
290
+ const initData = iface.encodeFunctionData("initialize", initArgs);
291
+
292
+ const proxy = await tronDeploy(tronWeb, artifacts, "ERC1967Proxy", [implementation.hex, initData], salt, feeLimit);
293
+ console.log(`${contractName} proxy: ${proxy.base58} (${proxy.hex})`);
294
+
295
+ return { proxy, implementation };
62
296
  }
63
297
 
64
- export async function getTronContract(
298
+ /** @internal Upgrade a UUPS proxy to a new implementation on Tron. */
299
+ export async function tronUpgradeProxy(
65
300
  tronWeb: any,
66
301
  artifacts: any,
67
302
  contractName: string,
68
- addr: string
303
+ proxyAddr: string,
304
+ feeLimit: number = 15_000_000_000
305
+ ): Promise<TronAddress> {
306
+ const impl = await tronDeploy(tronWeb, artifacts, contractName, [], "", feeLimit);
307
+ console.log(`${contractName} new implementation: ${impl.base58} (${impl.hex})`);
308
+
309
+ const artifact = await artifacts.readArtifact("BaseImplementation");
310
+ const proxyContract = await tronWeb.contract(artifact.abi, proxyAddr);
311
+ const oldImpl = await proxyContract.getImplementation().call();
312
+ console.log(`old implementation: ${tronFromHex(oldImpl)}`);
313
+
314
+ await sendAndWait(proxyContract.upgradeToAndCall(impl.hex, "0x"), tronWeb, { feeLimit });
315
+
316
+ const newImpl = await proxyContract.getImplementation().call();
317
+ console.log(`${contractName} upgraded: ${tronFromHex(oldImpl)} -> ${tronFromHex(newImpl)}`);
318
+ return impl;
319
+ }
320
+
321
+ /**
322
+ * Send a tron contract call and wait for on-chain confirmation.
323
+ * @param methodCall - tronweb contract method call (e.g. contract.setWtoken(addr))
324
+ * @param tronWeb - tronweb instance for querying tx status
325
+ * @param opts - send options (feeLimit, callValue, etc.)
326
+ */
327
+ export async function sendAndWait(
328
+ methodCall: any,
329
+ tronWeb: any,
330
+ opts: Record<string, any> = {}
69
331
  ): Promise<any> {
70
- console.log("operator address is:", tronWeb.defaultAddress.base58);
71
- let C = await artifacts.readArtifact(contractName);
72
- return tronWeb.contract(C.abi, addr);
332
+ const txId = await methodCall.send(opts);
333
+ return waitForTx(tronWeb, txId);
73
334
  }
74
335
 
75
- export function getTronDeployer(tronWeb: any, hex: boolean = false): string {
76
- if (hex) {
77
- return tronWeb.defaultAddress.hex.replace(/^(41)/, "0x");
336
+ /**
337
+ * Wait for a Tron transaction to be confirmed on-chain.
338
+ * @param tronWeb - tronweb instance
339
+ * @param txId - transaction hash returned by .send()
340
+ * @param retries - max poll attempts (default 20)
341
+ * @param interval - poll interval in ms (default 3000)
342
+ */
343
+ export async function waitForTx(
344
+ tronWeb: any,
345
+ txId: string,
346
+ retries: number = 20,
347
+ interval: number = 3000
348
+ ): Promise<any> {
349
+ console.log(`waiting for tx ${txId}...`);
350
+ for (let i = 0; i < retries; i++) {
351
+ const result = await tronWeb.trx.getTransactionInfo(txId);
352
+ if (result && result.id) {
353
+ if (result.receipt?.result === "SUCCESS") {
354
+ console.log(`tx confirmed in block ${result.blockNumber}`);
355
+ return result;
356
+ }
357
+ throw new Error(`tx failed: ${result.receipt?.result || "UNKNOWN"}`);
358
+ }
359
+ await new Promise(r => setTimeout(r, interval));
78
360
  }
79
- return tronWeb.defaultAddress.base58;
361
+ throw new Error(`tx not confirmed after ${retries} retries: ${txId}`);
80
362
  }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Contract verification — supports Etherscan, Blockscout (Mapo), and TronScan.
3
+ * Auto-routes based on network name.
4
+ */
5
+ import { isTronNetwork, tronFromHex } from "./tronHelper";
6
+
7
+ // TronScan API endpoints by chainId
8
+ const TRONSCAN_API: Record<number, string> = {
9
+ 728126428: "https://apilist.tronscan.org/api/solidity/contract/verify", // mainnet
10
+ 3448148188: "https://nile.tronscan.org/api/solidity/contract/verify", // nile testnet
11
+ };
12
+
13
+ export interface VerifyOptions {
14
+ address: string; // contract address
15
+ contractName: string; // e.g. "AuthorityManager"
16
+ contractPath?: string; // e.g. "contracts/AuthorityManager.sol:AuthorityManager"
17
+ constructorArgs?: any[]; // constructor arguments (raw values)
18
+ constructorParams?: string; // pre-encoded constructor params hex (without 0x), overrides constructorArgs
19
+ compiler?: string; // solc version, defaults to "0.8.25"
20
+ optimizer?: boolean; // defaults to true
21
+ optimizerRuns?: number; // defaults to 200
22
+ }
23
+
24
+ /**
25
+ * Unified contract verification — auto-routes to EVM (hardhat verify) or Tron (TronScan API)
26
+ */
27
+ export async function verify(hre: any, opts: VerifyOptions): Promise<void> {
28
+ const network = hre.network.name;
29
+
30
+ if (isTronNetwork(network)) {
31
+ await verifyTron(hre, opts);
32
+ } else {
33
+ await verifyEvm(hre, opts);
34
+ }
35
+ }
36
+
37
+ // ============================================================
38
+ // EVM verification via hardhat-verify
39
+ // ============================================================
40
+
41
+ async function verifyEvm(hre: any, opts: VerifyOptions): Promise<void> {
42
+ const contractPath = opts.contractPath || `contracts/${opts.contractName}.sol:${opts.contractName}`;
43
+
44
+ console.log(`verifying ${opts.contractName} at ${opts.address} ...`);
45
+
46
+ try {
47
+ await hre.run("verify:verify", {
48
+ contract: contractPath,
49
+ address: opts.address,
50
+ constructorArguments: opts.constructorArgs || [],
51
+ });
52
+ console.log(`${opts.contractName} verified`);
53
+ } catch (e: any) {
54
+ if (e.message?.includes("Already Verified")) {
55
+ console.log(`${opts.contractName} already verified`);
56
+ } else {
57
+ console.log(`verification failed: ${e.message || e}`);
58
+ // Print manual command as fallback
59
+ const args = (opts.constructorArgs || []).map((a: any) => typeof a === "string" ? `'${a}'` : a).join(" ");
60
+ console.log(`manual: npx hardhat verify --network ${hre.network.name} --contract ${contractPath} ${opts.address} ${args}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ // ============================================================
66
+ // Tron verification via TronScan API
67
+ // ============================================================
68
+
69
+ async function verifyTron(hre: any, opts: VerifyOptions): Promise<void> {
70
+ const chainId = hre.network.config.chainId;
71
+ const fs = require("fs");
72
+ const path = require("path");
73
+
74
+ // Auto-read compiler settings from hardhat config
75
+ const solcConfig = hre.config?.solidity?.compilers?.[0] || hre.config?.solidity || {};
76
+ const compiler = opts.compiler || solcConfig.version || "0.8.25";
77
+ const optimizer = opts.optimizer ?? solcConfig.settings?.optimizer?.enabled ?? true;
78
+ const optimizerRuns = opts.optimizerRuns ?? solcConfig.settings?.optimizer?.runs ?? 200;
79
+ const evmVersion = solcConfig.settings?.evmVersion || "london";
80
+ const viaIR = solcConfig.settings?.viaIR ? "1" : "0";
81
+
82
+ // Convert address to Tron format
83
+ let address = opts.address;
84
+ if (address.startsWith("0x")) {
85
+ address = tronFromHex(address);
86
+ }
87
+
88
+ // Generate flattened source
89
+ const outputDir = path.join(process.cwd(), "verify-output");
90
+ const flattenPath = path.join(outputDir, `${opts.contractName}_flatten.sol`);
91
+
92
+ console.log(`generating flatten for ${opts.contractName}...`);
93
+ try {
94
+ // Find actual source path from artifact
95
+ const artifact = await hre.artifacts.readArtifact(opts.contractName);
96
+ const sourcePath = artifact.sourceName; // e.g. "contracts/factory/Create2Factory.sol"
97
+ let flattenedSource = await hre.run("flatten:get-flattened-sources", {
98
+ files: [sourcePath],
99
+ });
100
+ flattenedSource = removeDuplicateSPDX(flattenedSource);
101
+ fs.mkdirSync(outputDir, { recursive: true });
102
+ fs.writeFileSync(flattenPath, flattenedSource);
103
+ console.log(`flatten saved to: ${flattenPath}`);
104
+ } catch (e) {
105
+ if (fs.existsSync(flattenPath)) {
106
+ console.log(`using existing flatten: ${flattenPath}`);
107
+ } else {
108
+ console.log(`flatten failed, generate manually: forge flatten contracts/${opts.contractName}.sol`);
109
+ return;
110
+ }
111
+ }
112
+
113
+ // Submit to TronScan API
114
+ const apiUrl = TRONSCAN_API[chainId];
115
+ if (!apiUrl) {
116
+ console.log(`no TronScan API for chainId ${chainId}, printing manual instructions instead`);
117
+ printTronVerifyInfo(address, opts.contractName, compiler, optimizer, optimizerRuns, evmVersion, flattenPath, chainId);
118
+ return;
119
+ }
120
+
121
+ console.log(`submitting verification to TronScan for ${address}...`);
122
+ try {
123
+ const FormData = require("form-data");
124
+ const form = new FormData();
125
+ form.append("contractAddress", address);
126
+ form.append("contractName", opts.contractName);
127
+ // Read full compiler version from build-info (includes commit hash)
128
+ let fullCompiler = `v${compiler}`;
129
+ try {
130
+ const buildInfoDir = path.join(process.cwd(), "artifacts/build-info");
131
+ const buildInfoFiles = fs.readdirSync(buildInfoDir);
132
+ if (buildInfoFiles.length > 0) {
133
+ const buildInfo = JSON.parse(fs.readFileSync(
134
+ path.join(buildInfoDir, buildInfoFiles[0]), "utf-8"
135
+ ));
136
+ if (buildInfo.solcLongVersion) {
137
+ fullCompiler = `v${buildInfo.solcLongVersion}`;
138
+ }
139
+ }
140
+ } catch (e: any) {
141
+ console.log(`[warn] could not read build-info: ${e.message}`);
142
+ }
143
+ form.append("compiler", fullCompiler);
144
+ form.append("license", "3"); // MIT
145
+ form.append("optimizer", optimizer ? "1" : "0");
146
+ form.append("runs", String(optimizerRuns));
147
+ form.append("viaIR", viaIR);
148
+ form.append("evmVersion", evmVersion);
149
+ // Encode constructor params from ABI if not pre-encoded
150
+ let constructorParams = opts.constructorParams || "";
151
+ if (!constructorParams && opts.constructorArgs && opts.constructorArgs.length > 0) {
152
+ const { Interface } = require("ethers");
153
+ const artifact = await hre.artifacts.readArtifact(opts.contractName);
154
+ const iface = new Interface(artifact.abi);
155
+ const encoded = iface.encodeDeploy(opts.constructorArgs);
156
+ constructorParams = encoded.slice(2); // remove 0x
157
+ }
158
+ form.append("constructorParams", constructorParams);
159
+ form.append("files", fs.createReadStream(flattenPath), {
160
+ filename: `${opts.contractName}.flat.tron.sol`,
161
+ contentType: "application/octet-stream",
162
+ });
163
+
164
+ const result: any = await new Promise((resolve, reject) => {
165
+ const url = new URL(apiUrl);
166
+ const https = require("https");
167
+ const req = https.request({
168
+ hostname: url.hostname,
169
+ path: url.pathname,
170
+ method: "POST",
171
+ headers: form.getHeaders(),
172
+ }, (res: any) => {
173
+ let data = "";
174
+ res.on("data", (chunk: string) => data += chunk);
175
+ res.on("end", () => {
176
+ try { resolve(JSON.parse(data)); } catch { resolve({ code: -1, errmsg: data }); }
177
+ });
178
+ });
179
+ req.on("error", reject);
180
+ form.pipe(req);
181
+ });
182
+
183
+ if (result.code === 200 || result.success) {
184
+ console.log(`${opts.contractName} verified on TronScan: ${result.data?.message || "success"}`);
185
+ } else {
186
+ console.log(`TronScan response:`, JSON.stringify(result, null, 2));
187
+ printTronVerifyInfo(address, opts.contractName, fullCompiler, optimizer, optimizerRuns, evmVersion, flattenPath, chainId);
188
+ }
189
+ } catch (e: any) {
190
+ console.log(`TronScan API error: ${e.message || e}`);
191
+ printTronVerifyInfo(address, opts.contractName, `v${compiler}`, optimizer, optimizerRuns, evmVersion, flattenPath, chainId);
192
+ }
193
+ }
194
+
195
+ function printTronVerifyInfo(
196
+ address: string, contractName: string, compiler: string,
197
+ optimizer: boolean, runs: number, evmVersion: string,
198
+ flattenPath: string, chainId: number
199
+ ) {
200
+ const isMainnet = chainId === 728126428;
201
+ const tronscanUrl = isMainnet
202
+ ? `https://tronscan.org/#/contract/${address}/code`
203
+ : `https://nile.tronscan.org/#/contract/${address}/code`;
204
+
205
+ console.log(`\n=== Verify manually on TronScan ===`);
206
+ console.log(`Address: ${address}`);
207
+ console.log(`Contract: ${contractName}`);
208
+ console.log(`Compiler: ${compiler}`);
209
+ console.log(`Optimizer: ${optimizer ? "enabled" : "disabled"}, runs: ${runs}`);
210
+ console.log(`EVM: ${evmVersion}`);
211
+ console.log(`Flatten: ${flattenPath}`);
212
+ console.log(`URL: ${tronscanUrl}`);
213
+ console.log(`=====================================\n`);
214
+ }
215
+
216
+ function removeDuplicateSPDX(source: string): string {
217
+ let found = false;
218
+ return source.split("\n").filter(line => {
219
+ if (line.includes("SPDX-License-Identifier")) {
220
+ if (found) return false;
221
+ found = true;
222
+ }
223
+ return true;
224
+ }).join("\n");
225
+ }