diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..20b25939ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/dist/ \ No newline at end of file diff --git a/packages/api/src/api/contract/contract.controller.spec.ts b/packages/api/src/api/contract/contract.controller.spec.ts index c02fc8ebd7..28633189ac 100644 --- a/packages/api/src/api/contract/contract.controller.spec.ts +++ b/packages/api/src/api/contract/contract.controller.spec.ts @@ -9,6 +9,12 @@ import { AddressService } from "../../address/address.service"; import { Address } from "../../address/address.entity"; import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto"; import { ContractController, parseAddressListPipeExceptionFactory } from "./contract.controller"; +import { SOURCE_CODE_EMPTY_INFO, mapContractSourceCode } from "../mappers/sourceCodeMapper"; + +jest.mock("../mappers/sourceCodeMapper", () => ({ + ...jest.requireActual("../mappers/sourceCodeMapper"), + mapContractSourceCode: jest.fn().mockReturnValue({ mockMappedSourceCode: true }), +})); describe("ContractController", () => { let controller: ContractController; @@ -180,27 +186,11 @@ describe("ContractController", () => { expect(response).toEqual({ status: ResponseStatus.OK, message: ResponseMessage.OK, - result: [ - { - ABI: "Contract source code not verified", - CompilerVersion: "", - ConstructorArguments: "", - ContractName: "", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "Unknown", - OptimizationUsed: "", - Proxy: "0", - Runs: "", - SourceCode: "", - SwarmSource: "", - }, - ], + result: [SOURCE_CODE_EMPTY_INFO], }); }); - it("returns empty source code response when contract verification info API fails with response status 404", async () => { + it("returns empty source code response when contract verification info is not found", async () => { pipeMock.mockImplementation((callback) => { return callback({ stack: "error stack", @@ -215,252 +205,42 @@ describe("ContractController", () => { expect(response).toEqual({ status: ResponseStatus.OK, message: ResponseMessage.OK, - result: [ - { - ABI: "Contract source code not verified", - CompilerVersion: "", - ConstructorArguments: "", - ContractName: "", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "Unknown", - OptimizationUsed: "", - Proxy: "0", - Runs: "", - SourceCode: "", - SwarmSource: "", - }, - ], - }); - }); - - it("returns contract source code from verification info API for solc compiler and single file contract", async () => { - const abi = []; - - pipeMock.mockReturnValue( - new rxjs.Observable((subscriber) => { - subscriber.next({ - data: { - artifacts: { - abi, - }, - request: { - sourceCode: "sourceCode", - constructorArguments: "0x0001", - contractName: "contractName", - optimizationUsed: false, - compilerSolcVersion: "8.10.0", - compilerZksolcVersion: "10.0.0", - }, - }, - }); - }) - ); - - const response = await controller.getContractSourceCode(address); - expect(response).toEqual({ - message: "OK", - result: [ - { - ABI: "[]", - CompilerVersion: "8.10.0", - CompilerZksolcVersion: "10.0.0", - ConstructorArguments: "0001", - ContractName: "contractName", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "", - OptimizationUsed: "0", - Proxy: "0", - Runs: "", - SourceCode: "sourceCode", - SwarmSource: "", - }, - ], - status: "1", + result: [SOURCE_CODE_EMPTY_INFO], }); - expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/info/${address}`); }); - it("returns contract source code from verification info API for solc compiler and multi file contract", async () => { - const abi = []; - - pipeMock.mockReturnValue( - new rxjs.Observable((subscriber) => { - subscriber.next({ - data: { - artifacts: { - abi, - }, - request: { - sourceCode: { - language: "Solidity", - settings: { - optimizer: { - enabled: true, - }, - }, - sources: { - "@openzeppelin/contracts/access/Ownable.sol": { - content: "Ownable.sol content", - }, - "faucet.sol": { - content: "faucet.sol content", - }, - }, - }, - constructorArguments: "0001", - contractName: "contractName", - optimizationUsed: true, - compilerSolcVersion: "8.10.0", - compilerZksolcVersion: "10.0.0", - }, - }, - }); - }) - ); - - const response = await controller.getContractSourceCode(address); - expect(response).toEqual({ - message: "OK", - result: [ - { - ABI: "[]", - CompilerVersion: "8.10.0", - CompilerZksolcVersion: "10.0.0", - ConstructorArguments: "0001", - ContractName: "contractName", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "", - OptimizationUsed: "1", - Proxy: "0", - Runs: "", - SourceCode: - '{{"language":"Solidity","settings":{"optimizer":{"enabled":true}},"sources":{"@openzeppelin/contracts/access/Ownable.sol":{"content":"Ownable.sol content"},"faucet.sol":{"content":"faucet.sol content"}}}}', - SwarmSource: "", - }, - ], - status: "1", - }); - expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/info/${address}`); - }); - - it("returns contract source code from verification info API for vyper compiler and single file contract", async () => { - const abi = []; + it("returns mapped source code for verified contract", async () => { + const data = { + artifacts: { + abi: [], + }, + request: { + sourceCode: "sourceCode", + constructorArguments: "0x0001", + contractName: "contractName", + optimizationUsed: false, + compilerSolcVersion: "8.10.0", + compilerZksolcVersion: "10.0.0", + }, + }; pipeMock.mockReturnValue( new rxjs.Observable((subscriber) => { subscriber.next({ - data: { - artifacts: { - abi, - }, - request: { - sourceCode: "sourceCode", - constructorArguments: "0x0001", - contractName: "contractName", - optimizationUsed: false, - compilerVyperVersion: "9.10.0", - compilerZkvyperVersion: "11.0.0", - }, - }, + data, }); }) ); const response = await controller.getContractSourceCode(address); - expect(response).toEqual({ - message: "OK", - result: [ - { - ABI: "[]", - CompilerVersion: "9.10.0", - CompilerZkvyperVersion: "11.0.0", - ConstructorArguments: "0001", - ContractName: "contractName", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "", - OptimizationUsed: "0", - Proxy: "0", - Runs: "", - SourceCode: "sourceCode", - SwarmSource: "", - }, - ], - status: "1", - }); + expect(mapContractSourceCode as jest.Mock).toHaveBeenCalledWith(data); + expect(mapContractSourceCode as jest.Mock).toHaveBeenCalledTimes(1); expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/info/${address}`); - }); - - it("returns contract source code from verification info API for vyper compiler and multi file contract", async () => { - const abi = []; - - pipeMock.mockReturnValue( - new rxjs.Observable((subscriber) => { - subscriber.next({ - data: { - artifacts: { - abi, - }, - request: { - sourceCode: { - language: "Vyper", - settings: { - optimizer: { - enabled: true, - }, - }, - sources: { - "Base.vy": { - content: "Base.vy content", - }, - "faucet.vy": { - content: "faucet.vy content", - }, - }, - }, - constructorArguments: "0001", - contractName: "contractName", - optimizationUsed: true, - compilerVyperVersion: "9.10.0", - compilerZkvyperVersion: "11.0.0", - }, - }, - }); - }) - ); - - const response = await controller.getContractSourceCode(address); expect(response).toEqual({ message: "OK", - result: [ - { - ABI: "[]", - CompilerVersion: "9.10.0", - CompilerZkvyperVersion: "11.0.0", - ConstructorArguments: "0001", - ContractName: "contractName", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "", - OptimizationUsed: "1", - Proxy: "0", - Runs: "", - SourceCode: - '{{"language":"Vyper","settings":{"optimizer":{"enabled":true}},"sources":{"Base.vy":{"content":"Base.vy content"},"faucet.vy":{"content":"faucet.vy content"}}}}', - SwarmSource: "", - }, - ], + result: [{ mockMappedSourceCode: true }], status: "1", }); - expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/info/${address}`); }); }); diff --git a/packages/api/src/api/contract/contract.controller.ts b/packages/api/src/api/contract/contract.controller.ts index 06ebfe605e..0f91a2cd81 100644 --- a/packages/api/src/api/contract/contract.controller.ts +++ b/packages/api/src/api/contract/contract.controller.ts @@ -20,6 +20,8 @@ import { ContractAbiResponseDto } from "../dtos/contract/contractAbiResponse.dto import { ContractSourceCodeResponseDto } from "../dtos/contract/contractSourceCodeResponse.dto"; import { ContractCreationResponseDto } from "../dtos/contract/contractCreationResponse.dto"; import { ApiExceptionFilter } from "../exceptionFilter"; +import { SOURCE_CODE_EMPTY_INFO, mapContractSourceCode } from "../mappers/sourceCodeMapper"; +import { ContractVerificationInfo } from "../types"; const entityName = "contract"; @@ -46,7 +48,7 @@ export class ContractController { public async getContractAbi( @Query("address", new ParseAddressPipe()) address: string ): Promise { - const { data } = await firstValueFrom( + const { data } = await firstValueFrom<{ data: ContractVerificationInfo }>( this.httpService.get(`${this.contractVerificationApiUrl}/contract_verification/info/${address}`).pipe( catchError((error: AxiosError) => { if (error.response?.status === 404) { @@ -75,7 +77,7 @@ export class ContractController { public async getContractSourceCode( @Query("address", new ParseAddressPipe()) address: string ): Promise { - const { data } = await firstValueFrom( + const { data } = await firstValueFrom<{ data: ContractVerificationInfo }>( this.httpService.get(`${this.contractVerificationApiUrl}/contract_verification/info/${address}`).pipe( catchError((error: AxiosError) => { this.logger.error({ @@ -90,63 +92,19 @@ export class ContractController { }) ) ); + if (!data?.artifacts?.abi) { return { status: ResponseStatus.OK, message: ResponseMessage.OK, - result: [ - { - ABI: "Contract source code not verified", - CompilerVersion: "", - ConstructorArguments: "", - ContractName: "", - EVMVersion: "Default", - Implementation: "", - Library: "", - LicenseType: "Unknown", - OptimizationUsed: "", - Proxy: "0", - Runs: "", - SourceCode: "", - SwarmSource: "", - }, - ], + result: [SOURCE_CODE_EMPTY_INFO], }; } + return { status: ResponseStatus.OK, message: ResponseMessage.OK, - result: [ - { - ...{ - ABI: JSON.stringify(data.artifacts.abi), - SourceCode: - typeof data.request.sourceCode === "string" - ? data.request.sourceCode - : `{${JSON.stringify(data.request.sourceCode)}}`, - // remove leading 0x as Etherscan does - ConstructorArguments: data.request.constructorArguments.startsWith("0x") - ? data.request.constructorArguments.substring(2) - : data.request.constructorArguments, - ContractName: data.request.contractName, - EVMVersion: "Default", - OptimizationUsed: data.request.optimizationUsed ? "1" : "0", - Library: "", - LicenseType: "", - CompilerVersion: data.request.compilerSolcVersion || data.request.compilerVyperVersion, - Runs: "", - SwarmSource: "", - Proxy: "0", - Implementation: "", - }, - ...(data.request.compilerZksolcVersion && { - CompilerZksolcVersion: data.request.compilerZksolcVersion, - }), - ...(data.request.compilerZkvyperVersion && { - CompilerZkvyperVersion: data.request.compilerZkvyperVersion, - }), - }, - ], + result: [mapContractSourceCode(data)], }; } diff --git a/packages/api/src/api/mappers/sourceCodeMapper.spec.ts b/packages/api/src/api/mappers/sourceCodeMapper.spec.ts new file mode 100644 index 0000000000..9e3764e06c --- /dev/null +++ b/packages/api/src/api/mappers/sourceCodeMapper.spec.ts @@ -0,0 +1,268 @@ +import { SOURCE_CODE_EMPTY_INFO, mapContractSourceCode } from "./sourceCodeMapper"; +import { ContractVerificationInfo } from "../types"; + +describe("SOURCE_CODE_EMPTY_INFO", () => { + it("returns ContractSourceCodeDto with empty or default values", () => { + expect(SOURCE_CODE_EMPTY_INFO).toEqual({ + ABI: "Contract source code not verified", + CompilerVersion: "", + ConstructorArguments: "", + ContractName: "", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "Unknown", + OptimizationUsed: "", + Proxy: "0", + Runs: "", + SourceCode: "", + SwarmSource: "", + }); + }); +}); + +describe("mapContractSourceCode", () => { + let verificationInfo: ContractVerificationInfo; + + beforeEach(() => { + verificationInfo = { + artifacts: { + abi: [], + bytecode: [0, 2, 0], + }, + request: { + id: 33877, + contractAddress: "0x000000000000000000000000000000000000800a", + codeFormat: "solidity-multi-file", + compilerSolcVersion: "0.8.17", + compilerZksolcVersion: "v1.3.9", + constructorArguments: "0x", + contractName: "Greeter", + optimizationUsed: true, + sourceCode: { + language: "Solidity", + settings: { + optimizer: { + enabled: true, + }, + }, + sources: { + fileName1: { + content: "fileName1 content", + }, + fileName2: { + content: "fileName2 content", + }, + }, + }, + }, + verifiedAt: "2023-07-24T10:36:11.121447608Z", + }; + }); + + it("returns mapped source code for Solidity multi file verification info", () => { + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.8.17", + CompilerZksolcVersion: "v1.3.9", + ConstructorArguments: "", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "1", + Proxy: "0", + Runs: "", + SourceCode: + '{{"language":"Solidity","settings":{"optimizer":{"enabled":true}},"sources":{"fileName1":{"content":"fileName1 content"},"fileName2":{"content":"fileName2 content"}}}}', + SwarmSource: "", + }); + }); + + it("returns mapped source code for Solidity single file verification info", () => { + verificationInfo.request = { + ...verificationInfo.request, + ...{ + codeFormat: "solidity-single-file", + sourceCode: "// source code", + }, + }; + + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.8.17", + CompilerZksolcVersion: "v1.3.9", + ConstructorArguments: "", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "1", + Proxy: "0", + Runs: "", + SourceCode: "// source code", + SwarmSource: "", + }); + }); + + it("returns mapped source code for Vyper multi file verification info with multiple files", () => { + verificationInfo.request = { + ...verificationInfo.request, + ...{ + codeFormat: "vyper-multi-file", + compilerSolcVersion: undefined, + compilerZksolcVersion: undefined, + compilerVyperVersion: "0.3.3", + compilerZkvyperVersion: "v1.3.9", + sourceCode: { + fileName1: "fileName1 content", + fileName2: "fileName2 content", + }, + }, + }; + + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.3.3", + CompilerZkvyperVersion: "v1.3.9", + ConstructorArguments: "", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "1", + Proxy: "0", + Runs: "", + SourceCode: + '{{"language":"Vyper","settings":{"optimizer":{"enabled":true}},"sources":{"fileName1":{"content":"fileName1 content"},"fileName2":{"content":"fileName2 content"}}}}', + SwarmSource: "", + }); + }); + + it("returns proper optimizer for Vyper multi file verification info with multiple files", () => { + verificationInfo.request = { + ...verificationInfo.request, + ...{ + codeFormat: "vyper-multi-file", + compilerSolcVersion: undefined, + compilerZksolcVersion: undefined, + compilerVyperVersion: "0.3.3", + compilerZkvyperVersion: "v1.3.9", + sourceCode: { + fileName1: "fileName1 content", + fileName2: "fileName2 content", + }, + optimizationUsed: false, + }, + }; + + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.3.3", + CompilerZkvyperVersion: "v1.3.9", + ConstructorArguments: "", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "0", + Proxy: "0", + Runs: "", + SourceCode: + '{{"language":"Vyper","settings":{"optimizer":{"enabled":false}},"sources":{"fileName1":{"content":"fileName1 content"},"fileName2":{"content":"fileName2 content"}}}}', + SwarmSource: "", + }); + }); + + it("returns mapped source code for Vyper multi file verification info with single file", () => { + verificationInfo.request = { + ...verificationInfo.request, + ...{ + codeFormat: "vyper-multi-file", + compilerSolcVersion: undefined, + compilerZksolcVersion: undefined, + compilerVyperVersion: "0.3.3", + compilerZkvyperVersion: "v1.3.9", + sourceCode: { + fileName1: "fileName1 content", + }, + }, + }; + + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.3.3", + CompilerZkvyperVersion: "v1.3.9", + ConstructorArguments: "", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "1", + Proxy: "0", + Runs: "", + SourceCode: "fileName1 content", + SwarmSource: "", + }); + }); + + it("returns contructor arguments without 0x", () => { + verificationInfo.request = { + ...verificationInfo.request, + ...{ + constructorArguments: "123", + }, + }; + + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.8.17", + CompilerZksolcVersion: "v1.3.9", + ConstructorArguments: "123", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "1", + Proxy: "0", + Runs: "", + SourceCode: + '{{"language":"Solidity","settings":{"optimizer":{"enabled":true}},"sources":{"fileName1":{"content":"fileName1 content"},"fileName2":{"content":"fileName2 content"}}}}', + SwarmSource: "", + }); + }); + + it("returns OptimizationUsed depending on verification info", () => { + verificationInfo.request = { + ...verificationInfo.request, + ...{ + optimizationUsed: false, + }, + }; + (verificationInfo.request.sourceCode as any).settings.optimizer.enabled = false; + + expect(mapContractSourceCode(verificationInfo)).toEqual({ + ABI: "[]", + CompilerVersion: "0.8.17", + CompilerZksolcVersion: "v1.3.9", + ConstructorArguments: "", + ContractName: "Greeter", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "", + OptimizationUsed: "0", + Proxy: "0", + Runs: "", + SourceCode: + '{{"language":"Solidity","settings":{"optimizer":{"enabled":false}},"sources":{"fileName1":{"content":"fileName1 content"},"fileName2":{"content":"fileName2 content"}}}}', + SwarmSource: "", + }); + }); +}); diff --git a/packages/api/src/api/mappers/sourceCodeMapper.ts b/packages/api/src/api/mappers/sourceCodeMapper.ts new file mode 100644 index 0000000000..c720898402 --- /dev/null +++ b/packages/api/src/api/mappers/sourceCodeMapper.ts @@ -0,0 +1,73 @@ +import { ContractVerificationInfo } from "../types"; +import { ContractSourceCodeDto } from "../dtos/contract/contractSourceCodeResponse.dto"; + +export const SOURCE_CODE_EMPTY_INFO: ContractSourceCodeDto = { + ABI: "Contract source code not verified", + CompilerVersion: "", + ConstructorArguments: "", + ContractName: "", + EVMVersion: "Default", + Implementation: "", + Library: "", + LicenseType: "Unknown", + OptimizationUsed: "", + Proxy: "0", + Runs: "", + SourceCode: "", + SwarmSource: "", +}; + +export const mapContractSourceCode = (data: ContractVerificationInfo): ContractSourceCodeDto => { + let sourceCode: string; + if (data.request.codeFormat.startsWith("vyper-multi-file")) { + const vyperFileNames = Object.keys(data.request.sourceCode); + if (vyperFileNames.length === 1) { + sourceCode = data.request.sourceCode[vyperFileNames[0]]; + } else { + const mappedSourceCode = { + language: "Vyper", + settings: { + optimizer: { + enabled: data.request.optimizationUsed, + }, + }, + sources: vyperFileNames.reduce>((sources, fileName) => { + return { ...sources, ...{ [fileName]: { content: data.request.sourceCode[fileName] } } }; + }, {}), + }; + sourceCode = `{${JSON.stringify(mappedSourceCode)}}`; + } + } else { + sourceCode = + typeof data.request.sourceCode === "string" + ? data.request.sourceCode + : `{${JSON.stringify(data.request.sourceCode)}}`; + } + + return { + ...{ + ABI: JSON.stringify(data.artifacts.abi), + SourceCode: sourceCode, + // remove leading 0x as Etherscan does + ConstructorArguments: data.request.constructorArguments.startsWith("0x") + ? data.request.constructorArguments.substring(2) + : data.request.constructorArguments, + ContractName: data.request.contractName, + EVMVersion: "Default", + OptimizationUsed: data.request.optimizationUsed ? "1" : "0", + Library: "", + LicenseType: "", + CompilerVersion: data.request.compilerSolcVersion || data.request.compilerVyperVersion, + Runs: "", + SwarmSource: "", + Proxy: "0", + Implementation: "", + }, + ...(data.request.compilerZksolcVersion && { + CompilerZksolcVersion: data.request.compilerZksolcVersion, + }), + ...(data.request.compilerZkvyperVersion && { + CompilerZkvyperVersion: data.request.compilerZkvyperVersion, + }), + }; +}; diff --git a/packages/api/src/api/types.ts b/packages/api/src/api/types.ts index b334e3c7e2..039eb00ac3 100644 --- a/packages/api/src/api/types.ts +++ b/packages/api/src/api/types.ts @@ -44,3 +44,61 @@ export const apiActionsMap = { [ApiModule.Transaction]: Object.values(ApiTransactionAction) as string[], [ApiModule.Block]: Object.values(ApiBlockAction) as string[], }; + +type ContractFunctionInput = { + internalType: string; + name: string; + type: string; + value?: string | number; +}; +type ContractFunctionOutput = { + internalType: string; + name: string; + type: string; +}; + +export type AbiFragment = { + inputs: ContractFunctionInput[]; + name: string; + outputs: ContractFunctionOutput[]; + stateMutability: string; + type: string; +}; + +type ContractVerificationRequest = { + id: number; + codeFormat: string; + contractName: string; + contractAddress: string; + compilerSolcVersion?: string; + compilerZksolcVersion?: string; + compilerVyperVersion?: string; + compilerZkvyperVersion?: string; + constructorArguments: string; + sourceCode: + | string + | { + language: string; + settings: { + optimizer: { + enabled: boolean; + }; + }; + sources: { + [key: string]: { + content: string; + }; + }; + } + | Record; + optimizationUsed: boolean; +}; + +export type ContractVerificationInfo = { + artifacts: { + abi: AbiFragment[]; + bytecode: number[]; + }; + request: ContractVerificationRequest; + verifiedAt: string; +};