@mapprotocol/common-contracts 0.1.1 → 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/README.md +114 -58
- package/artifacts/contracts/AuthorityManager.sol/AuthorityManager.json +2 -2
- package/artifacts/contracts/factory/Create2Factory.sol/Create2Factory.json +107 -0
- package/contracts/factory/Create2Factory.sol +57 -0
- package/package.json +15 -3
- package/typechain-types/contracts/factory/Create2Factory.ts +174 -0
- package/typechain-types/contracts/factory/index.ts +4 -0
- package/typechain-types/contracts/index.ts +2 -0
- package/typechain-types/factories/@openzeppelin/contracts/access/manager/AccessManager__factory.ts +1 -1
- package/typechain-types/factories/@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils__factory.ts +1 -1
- package/typechain-types/factories/@openzeppelin/contracts/utils/Address__factory.ts +1 -1
- package/typechain-types/factories/@openzeppelin/contracts/utils/Errors__factory.ts +1 -1
- package/typechain-types/factories/@openzeppelin/contracts/utils/math/SafeCast__factory.ts +1 -1
- package/typechain-types/factories/contracts/AuthorityManager__factory.ts +1 -1
- package/typechain-types/factories/contracts/factory/Create2Factory__factory.ts +163 -0
- package/typechain-types/factories/contracts/factory/index.ts +4 -0
- package/typechain-types/factories/contracts/index.ts +1 -0
- package/typechain-types/hardhat.d.ts +18 -0
- package/typechain-types/index.ts +2 -0
- package/utils/addressCodec.ts +57 -0
- package/utils/codeHash.ts +75 -0
- package/utils/deployRecord.ts +117 -0
- package/utils/deployer.ts +123 -0
- package/utils/evmHelper.ts +108 -0
- package/utils/factory.ts +210 -0
- package/utils/index.ts +42 -0
- package/utils/tronHelper.ts +362 -0
- package/utils/verifier.ts +225 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
let TronWeb = require("tronweb");
|
|
2
|
+
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Pure address conversion — no RPC, no private key needed
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
/** Convert hex address to Tron base58 format. Returns as-is if already base58. */
|
|
8
|
+
export function tronFromHex(hex: string): string {
|
|
9
|
+
if (hex.startsWith("T") && hex.length === 34) return hex;
|
|
10
|
+
return TronWeb.address.fromHex(hex);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Convert Tron base58 address to 0x-prefixed hex. Returns as-is if already hex. */
|
|
14
|
+
export function tronToHex(addr: string): string {
|
|
15
|
+
if (addr.startsWith("0x") && addr.length === 42) return addr;
|
|
16
|
+
return TronWeb.address.toHex(addr).replace(/^(41)/, "0x");
|
|
17
|
+
}
|
|
18
|
+
|
|
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
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// TronClient
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
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;
|
|
221
|
+
|
|
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
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================
|
|
236
|
+
// Low-level functions (used internally by TronClient)
|
|
237
|
+
// ============================================================
|
|
238
|
+
|
|
239
|
+
/** @internal Deploy contract on Tron. If salt is provided, uses CREATE2 factory. */
|
|
240
|
+
export async function tronDeploy(
|
|
241
|
+
tronWeb: any,
|
|
242
|
+
artifacts: any,
|
|
243
|
+
contractName: string,
|
|
244
|
+
args: any[] = [],
|
|
245
|
+
salt: string = "",
|
|
246
|
+
feeLimit: number = 15_000_000_000
|
|
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);
|
|
255
|
+
console.log("deploy address is:", tronWeb.defaultAddress.base58);
|
|
256
|
+
const instance = await tronWeb.contract().new({
|
|
257
|
+
abi: artifact.abi,
|
|
258
|
+
bytecode: artifact.bytecode,
|
|
259
|
+
feeLimit,
|
|
260
|
+
callValue: 0,
|
|
261
|
+
parameters: args,
|
|
262
|
+
});
|
|
263
|
+
const rawAddress = instance.address; // 41-prefixed hex
|
|
264
|
+
const hex = rawAddress.replace(/^41/, "0x");
|
|
265
|
+
const base58 = tronWeb.address.fromHex(rawAddress);
|
|
266
|
+
console.log(`${contractName} deployed: ${base58} (${hex})`);
|
|
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 };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** @internal Upgrade a UUPS proxy to a new implementation on Tron. */
|
|
299
|
+
export async function tronUpgradeProxy(
|
|
300
|
+
tronWeb: any,
|
|
301
|
+
artifacts: any,
|
|
302
|
+
contractName: 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> = {}
|
|
331
|
+
): Promise<any> {
|
|
332
|
+
const txId = await methodCall.send(opts);
|
|
333
|
+
return waitForTx(tronWeb, txId);
|
|
334
|
+
}
|
|
335
|
+
|
|
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));
|
|
360
|
+
}
|
|
361
|
+
throw new Error(`tx not confirmed after ${retries} retries: ${txId}`);
|
|
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
|
+
}
|