import { TronWeb } from '../../tronweb.js'; import utils from '../../utils/index.js'; import { Method, AbiFragmentNoErrConstructor } from './method.js'; import { buildReadNamespace, buildWriteNamespace } from './readWrite.js'; import { clonePlainData } from '../../utils/clone.js'; import type { ContractReadNamespace, ContractWriteNamespace } from '../../types/Contract.js'; import type { ContractAbiInterface, GetMethodsTypeFromAbi, GetOnMethodTypeFromAbi, AnyOnMethodType } from '../../types/ABI.js'; import type { Address } from '../../types/Trx.js'; import type { CreateSmartContractOptions } from '../../types/TransactionBuilder.js'; // When the ABI itself defines a function named `read`/`write`, the legacy flat // call surface must stay reachable as `contract.read(...)`/`contract.write(...)`. // Wrap the legacy call in a Proxy whose property access and enumeration are // served entirely by the namespace, so both surfaces share the one getter. function wrapReservedNamespace(namespace: T, legacyCall: unknown): T { if (typeof legacyCall !== 'function') return namespace; return new Proxy(legacyCall, { get: (_target, prop) => namespace[prop as keyof T], has: (_target, prop) => prop in namespace, ownKeys: () => Reflect.ownKeys(namespace), getOwnPropertyDescriptor: (_target, prop) => Object.getOwnPropertyDescriptor(namespace, prop), }) as T; } // The contract works on a plain copy of the ABI it is given, so the caller's fragments are // read once, when the ABI is loaded, and are never modified. function cloneAbi(abi: Abi): Abi { return clonePlainData(abi, { root: 'abi', invalid: (reason, path) => new Error(`Invalid ABI provided: ${reason} at ${path}`), }); } export class Contract { tronWeb: TronWeb; abi: Abi; address: false | string; eventListener: any; bytecode?: false | string; deployed?: boolean; lastBlock?: false | number; methods: GetOnMethodTypeFromAbi & AnyOnMethodType; methodInstances: GetMethodsTypeFromAbi; props: any[]; private _readNamespace?: { abi: Abi; namespace: ContractReadNamespace }; private _writeNamespace?: { abi: Abi; namespace: ContractWriteNamespace }; [key: string | number | symbol]: any; constructor(tronWeb: TronWeb, abi: Abi = [] as any, address: Address) { if (!tronWeb || !(tronWeb instanceof TronWeb)) throw new Error('Expected instance of TronWeb'); this.tronWeb = tronWeb; this.address = address; this.abi = abi; this.eventListener = false; this.bytecode = false; this.deployed = false; this.lastBlock = false; this.methods = Object.create(null) as GetOnMethodTypeFromAbi & AnyOnMethodType; this.methodInstances = Object.create(null) as GetMethodsTypeFromAbi; this.props = []; if (utils.address.isAddress(address)) { this.deployed = true; } else { this.address = false; } this.loadAbi(abi); } /** * Type-safe read namespace: * every `view`/`pure` ABI function is callable as * `contract.read.fn([args], { from, value })` and resolves to the decoded * result directly. Rebuilt lazily whenever the contract ABI is replaced. */ get read(): ContractReadNamespace { if (!this._readNamespace || this._readNamespace.abi !== this.abi) { this._readNamespace = { abi: this.abi, namespace: wrapReservedNamespace( buildReadNamespace(this) as object, this.methods['read'] ) as ContractReadNamespace, }; } return this._readNamespace.namespace; } /** * Type-safe write namespace: * every state-changing ABI function is callable as * `contract.write.fn([args], { account, value, feeLimit, tokenId, tokenValue, permissionId })`, * signed with the instance's default private key, broadcast, and resolved to * the transaction ID. Rebuilt lazily whenever the contract ABI is replaced. */ get write(): ContractWriteNamespace { if (!this._writeNamespace || this._writeNamespace.abi !== this.abi) { this._writeNamespace = { abi: this.abi, namespace: wrapReservedNamespace( buildWriteNamespace(this) as object, this.methods['write'] ) as ContractWriteNamespace, }; } return this._writeNamespace.namespace; } hasProperty(property: number | string | symbol) { return ( Object.prototype.hasOwnProperty.call(this, property) || Object.prototype.hasOwnProperty.call(Object.getPrototypeOf(this), property) ); } loadAbi(abi: Abi) { this.abi = cloneAbi(abi); this.methods = Object.create(null) as GetOnMethodTypeFromAbi & AnyOnMethodType; this.methodInstances = Object.create(null) as GetMethodsTypeFromAbi; this.props.forEach((prop: string) => delete (this as any)[prop]); this.props = []; this.abi.forEach((func) => { // Don't build a method for constructor function. That's handled through contract create. // Don't build a method for error function. if (!func.type || ['constructor', 'error'].includes(func.type.toLowerCase())) return; const method = new Method(this, func as AbiFragmentNoErrConstructor) as GetMethodsTypeFromAbi[keyof GetMethodsTypeFromAbi]; const methodCall = method.onMethod.bind(method); const { name, functionSelector, signature } = method; const internalName = name as keyof GetMethodsTypeFromAbi; const internalFunctionSelector = functionSelector as keyof GetMethodsTypeFromAbi; const internalSignature = signature as keyof GetMethodsTypeFromAbi; (this.methods as AnyOnMethodType)[name] = methodCall; (this.methods as AnyOnMethodType)[functionSelector!] = methodCall; (this.methods as AnyOnMethodType)[signature] = methodCall; this.methodInstances[internalName] = method; this.methodInstances[internalFunctionSelector!] = method; this.methodInstances[internalSignature] = method; if (!this.hasProperty(name)) { (this as any)[name] = methodCall; this.props.push(name); } if (!this.hasProperty(functionSelector!)) { (this as any)[functionSelector!] = methodCall; this.props.push(functionSelector); } if (!this.hasProperty(signature)) { (this as any)[signature] = methodCall; this.props.push(signature); } }); } decodeInput(data: string) { const methodName = data.substring(0, 8); const inputData = data.substring(8); if (!this.methodInstances[methodName]) throw new Error('Contract method ' + methodName + ' not found'); const methodInstance = this.methodInstances[methodName]; return { name: methodInstance.name, params: this.methodInstances[methodName].decodeInput(inputData), }; } async new(options: CreateSmartContractOptions, privateKey = this.tronWeb.defaultPrivateKey) { const address = this.tronWeb.address.fromPrivateKey(privateKey as string); const transaction = await this.tronWeb.transactionBuilder.createSmartContract(options, address as Address); const signedTransaction = await this.tronWeb.trx.sign(transaction, privateKey); const contract = await this.tronWeb.trx.sendRawTransaction(signedTransaction); if (contract.code) { throw { error: contract.code, message: this.tronWeb.toUtf8(contract.message), }; } await utils.sleep(3000); const abi = signedTransaction.raw_data.contract[0].parameter.value.new_contract.abi.entrys || []; return this.tronWeb.contract(abi, signedTransaction.contract_address); } async at(contractAddress: Address) { try { const contract = await this.tronWeb.trx.getContract(contractAddress); if (!contract.contract_address) { throw new Error('Unknown error: ' + JSON.stringify(contract, null, 2)); } this.address = contract.contract_address; this.bytecode = contract.bytecode; this.deployed = true; this.loadAbi(contract.abi ? (contract.abi.entrys ? contract.abi.entrys : []) : []); return this; } catch (ex: any) { if (ex.toString().includes('does not exist')) { throw new Error('Contract has not been deployed on the network'); } throw new Error(ex); } } } export type { CallOptions, SendOptions, AbiFragmentNoErrConstructor } from './method.js'; export { Method } from './method.js'; export type { ContractReadNamespace, ContractWriteNamespace, ReadContractFunctionName, WriteContractFunctionName, ReadContractParameters, ReadContractReturnType, WriteContractParameters, WriteContractReturnType, ReadOptions, WriteOptions, CollapseSingleItemTuple, FunctionSelector, } from '../../types/Contract.js'; export { buildReadNamespace, buildWriteNamespace } from './readWrite.js';