diff --git a/packages/api/src/api/api.controller.spec.ts b/packages/api/src/api/api.controller.spec.ts index a5bf7414b6..fc2a8d7001 100644 --- a/packages/api/src/api/api.controller.spec.ts +++ b/packages/api/src/api/api.controller.spec.ts @@ -66,6 +66,13 @@ describe("ApiController", () => { }); }); + describe("getVerificationStatus", () => { + it("returns null as it is defined only to appear in docs and cannot be called", async () => { + const result = await controller.getVerificationStatus(); + expect(result).toBe(null); + }); + }); + describe("getContractCreation", () => { it("returns null as it is defined only to appear in docs and cannot be called", async () => { const result = await controller.getContractCreation(); diff --git a/packages/api/src/api/api.controller.ts b/packages/api/src/api/api.controller.ts index 55c95ec829..ef950f075d 100644 --- a/packages/api/src/api/api.controller.ts +++ b/packages/api/src/api/api.controller.ts @@ -16,6 +16,7 @@ import { ContractCreationResponseDto, ContractCreationInfoDto } from "./dtos/con import { ContractSourceCodeResponseDto } from "./dtos/contract/contractSourceCodeResponse.dto"; import { VerifyContractRequestDto } from "./dtos/contract/verifyContractRequest.dto"; import { VerifyContractResponseDto } from "./dtos/contract/verifyContractResponse.dto"; +import { ContractVerificationStatusResponseDto } from "./dtos/contract/contractVerificationStatusResponse.dto"; import { TransactionStatusResponseDto, TransactionStatusDto } from "./dtos/transaction/transactionStatusResponse.dto"; import { TransactionReceiptStatusResponseDto } from "./dtos/transaction/transactionReceiptStatusResponse.dto"; import { AccountTransactionDto } from "./dtos/account/accountTransaction.dto"; @@ -109,6 +110,26 @@ export class ApiController { return null; } + @ApiTags("Contract API") + @Get("api?module=contract&action=getcontractcreation") + @ApiOperation({ summary: "Fetch creation details for a list of contract addresses" }) + @ApiQuery({ + isArray: true, + explode: false, + name: "contractaddresses", + description: "List of contract addresses, up to 5 at a time", + example: ["0x8A63F953e19aA4Ce3ED90621EeF61E17A95c6594", "0x0E03197d697B592E5AE49EC14E952cddc9b28e14"], + required: true, + }) + @ApiExtraModels(ContractCreationInfoDto) + @ApiOkResponse({ + description: "Contract creation information", + type: ContractCreationResponseDto, + }) + public async getContractCreation(): Promise { + return null; + } + @ApiTags("Contract API") @Post("api") @ApiOperation({ summary: "Submits a contract source code for verification" }) @@ -122,22 +143,19 @@ export class ApiController { } @ApiTags("Contract API") - @Get("api?module=contract&action=getcontractcreation") - @ApiOperation({ summary: "Fetch creation details for a list of contract addresses" }) + @Get("api?module=contract&action=checkverifystatus") + @ApiOperation({ summary: "Check source code verification submission status" }) @ApiQuery({ - isArray: true, - explode: false, - name: "contractaddresses", - description: "List of contract addresses, up to 5 at a time", - example: ["0x8A63F953e19aA4Ce3ED90621EeF61E17A95c6594", "0x0E03197d697B592E5AE49EC14E952cddc9b28e14"], + name: "guid", + description: "Verification ID", + example: "44071", required: true, }) - @ApiExtraModels(ContractCreationInfoDto) @ApiOkResponse({ - description: "Contract creation information", - type: ContractCreationResponseDto, + description: "Source code verification status", + type: ContractVerificationStatusResponseDto, }) - public async getContractCreation(): Promise { + public async getVerificationStatus(): Promise { return null; } diff --git a/packages/api/src/api/contract/contract.controller.spec.ts b/packages/api/src/api/contract/contract.controller.spec.ts index 8c69d54a62..47761405e8 100644 --- a/packages/api/src/api/contract/contract.controller.spec.ts +++ b/packages/api/src/api/contract/contract.controller.spec.ts @@ -7,7 +7,7 @@ import { AxiosResponse, AxiosError } from "axios"; import * as rxjs from "rxjs"; import { AddressService } from "../../address/address.service"; import { Address } from "../../address/address.entity"; -import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto"; +import { ResponseStatus, ResponseMessage, ResponseResultMessage } from "../dtos/common/responseBase.dto"; import { ContractController, parseAddressListPipeExceptionFactory } from "./contract.controller"; import { VerifyContractRequestDto } from "../dtos/contract/verifyContractRequest.dto"; import { SOURCE_CODE_EMPTY_INFO, mapContractSourceCode } from "../mappers/sourceCodeMapper"; @@ -518,6 +518,158 @@ describe("ContractController", () => { }); }); + describe("getVerificationStatus", () => { + let pipeMock = jest.fn(); + const verificationId = "1234"; + + beforeEach(() => { + pipeMock = jest.fn(); + jest.spyOn(httpServiceMock, "get").mockReturnValue({ + pipe: pipeMock, + } as unknown as rxjs.Observable); + jest.spyOn(rxjs, "catchError").mockImplementation((callback) => callback as any); + }); + + it("throws error when contract verification API fails with response status different to 404", async () => { + pipeMock.mockImplementation((callback) => { + callback({ + stack: "error stack", + response: { + data: "response data", + status: 500, + }, + } as AxiosError); + }); + + await expect(controller.getVerificationStatus(verificationId)).rejects.toThrowError( + new InternalServerErrorException("Failed to get verification status") + ); + }); + + it("throws error when contract verification info API fails with response status 404", async () => { + pipeMock.mockImplementation((callback) => { + callback({ + stack: "error stack", + response: { + data: "response data", + status: 404, + }, + } as AxiosError); + }); + + await expect(controller.getVerificationStatus(address)).rejects.toThrowError( + new InternalServerErrorException("Contract verification submission not found") + ); + }); + + it("throws error when contract verification info API fails without response data", async () => { + pipeMock.mockImplementation((callback) => { + callback({ + stack: "error stack", + } as AxiosError); + }); + + await expect(controller.getVerificationStatus(address)).rejects.toThrowError( + new InternalServerErrorException("Failed to get verification status") + ); + }); + + it("throws error when contract verification is not specified", async () => { + pipeMock.mockReturnValue( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: {}, + }); + }) + ); + + await expect(controller.getVerificationStatus(undefined)).rejects.toThrowError( + new BadRequestException("Verification ID is not specified") + ); + }); + + it("returns successful verification status", async () => { + pipeMock.mockReturnValue( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: { + status: "successful", + }, + }); + }) + ); + + const response = await controller.getVerificationStatus(verificationId); + expect(response).toEqual({ + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: ResponseResultMessage.VERIFICATION_SUCCESSFUL, + }); + expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/${verificationId}`); + }); + + it("returns queued verification status", async () => { + pipeMock.mockReturnValue( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: { + status: "queued", + }, + }); + }) + ); + + const response = await controller.getVerificationStatus(verificationId); + expect(response).toEqual({ + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: ResponseResultMessage.VERIFICATION_QUEUED, + }); + expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/${verificationId}`); + }); + + it("returns in progress verification status", async () => { + pipeMock.mockReturnValue( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: { + status: "in_progress", + }, + }); + }) + ); + + const response = await controller.getVerificationStatus(verificationId); + expect(response).toEqual({ + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: ResponseResultMessage.VERIFICATION_IN_PROGRESS, + }); + expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/${verificationId}`); + }); + + it("returns in progress verification status", async () => { + pipeMock.mockReturnValue( + new rxjs.Observable((subscriber) => { + subscriber.next({ + data: { + status: "failed", + error: "ERROR! Compilation error.", + }, + }); + }) + ); + + const response = await controller.getVerificationStatus(verificationId); + expect(response).toEqual({ + status: ResponseStatus.NOTOK, + message: ResponseMessage.NOTOK, + result: "ERROR! Compilation error.", + }); + expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/${verificationId}`); + }); + }); + describe("getContractCreation", () => { it("thrown an error when called with more than 5 addresses", async () => { await expect( diff --git a/packages/api/src/api/contract/contract.controller.ts b/packages/api/src/api/contract/contract.controller.ts index bae4ed847d..699ffe0467 100644 --- a/packages/api/src/api/contract/contract.controller.ts +++ b/packages/api/src/api/contract/contract.controller.ts @@ -18,14 +18,19 @@ import { AxiosError } from "axios"; import { catchError, firstValueFrom, of } from "rxjs"; import { AddressService } from "../../address/address.service"; import { ParseAddressPipe } from "../../common/pipes/parseAddress.pipe"; -import { ResponseStatus, ResponseMessage } from "../dtos/common/responseBase.dto"; +import { ResponseStatus, ResponseMessage, ResponseResultMessage } from "../dtos/common/responseBase.dto"; import { ContractAbiResponseDto } from "../dtos/contract/contractAbiResponse.dto"; import { ContractSourceCodeResponseDto } from "../dtos/contract/contractSourceCodeResponse.dto"; import { ContractCreationResponseDto } from "../dtos/contract/contractCreationResponse.dto"; import { VerifyContractRequestDto } from "../dtos/contract/verifyContractRequest.dto"; +import { ContractVerificationStatusResponseDto } from "../dtos/contract/contractVerificationStatusResponse.dto"; import { ApiExceptionFilter } from "../exceptionFilter"; import { SOURCE_CODE_EMPTY_INFO, mapContractSourceCode } from "../mappers/sourceCodeMapper"; -import { ContractVerificationInfo, ContractVerificationCodeFormatEnum } from "../types"; +import { + ContractVerificationInfo, + ContractVerificationCodeFormatEnum, + ContractVerificationStatusResponse, +} from "../types"; import { VerifyContractResponseDto } from "../dtos/contract/verifyContractResponse.dto"; const entityName = "contract"; @@ -233,4 +238,59 @@ export class ContractController { result: result.length ? result : null, }; } + + @Get("/checkverifystatus") + public async getVerificationStatus( + @Query("guid") verificationId: string + ): Promise { + if (!verificationId) { + throw new BadRequestException("Verification ID is not specified"); + } + + const { data } = await firstValueFrom<{ data: ContractVerificationStatusResponse }>( + this.httpService.get(`${this.contractVerificationApiUrl}/contract_verification/${verificationId}`).pipe( + catchError((error: AxiosError) => { + if (error.response?.status === 404) { + throw new BadRequestException("Contract verification submission not found"); + } + this.logger.error({ + message: "Error fetching contract verification status", + stack: error.stack, + response: error.response?.data, + }); + throw new InternalServerErrorException("Failed to get verification status"); + }) + ) + ); + + if (data.status === "successful") { + return { + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: ResponseResultMessage.VERIFICATION_SUCCESSFUL, + }; + } + + if (data.status === "queued") { + return { + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: ResponseResultMessage.VERIFICATION_QUEUED, + }; + } + + if (data.status === "in_progress") { + return { + status: ResponseStatus.OK, + message: ResponseMessage.OK, + result: ResponseResultMessage.VERIFICATION_IN_PROGRESS, + }; + } + + return { + status: ResponseStatus.NOTOK, + message: ResponseMessage.NOTOK, + result: data.error, + }; + } } diff --git a/packages/api/src/api/dtos/common/responseBase.dto.ts b/packages/api/src/api/dtos/common/responseBase.dto.ts index 8232b0226b..4b3f4488ca 100644 --- a/packages/api/src/api/dtos/common/responseBase.dto.ts +++ b/packages/api/src/api/dtos/common/responseBase.dto.ts @@ -15,6 +15,9 @@ export enum ResponseMessage { export enum ResponseResultMessage { INVALID_PARAM = "Error! Invalid parameter", + VERIFICATION_SUCCESSFUL = "Pass - Verified", + VERIFICATION_QUEUED = "Pending in queue", + VERIFICATION_IN_PROGRESS = "In progress", } export class ResponseBaseDto { diff --git a/packages/api/src/api/dtos/contract/contractVerificationStatusResponse.dto.ts b/packages/api/src/api/dtos/contract/contractVerificationStatusResponse.dto.ts new file mode 100644 index 0000000000..432e0f14d6 --- /dev/null +++ b/packages/api/src/api/dtos/contract/contractVerificationStatusResponse.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { ResponseBaseDto } from "../common/responseBase.dto"; + +export class ContractVerificationStatusResponseDto extends ResponseBaseDto { + @ApiProperty({ + description: "Verification result explanation", + example: "Pass - Verified", + examples: ["Pass - Verified", "Fail - Unable to verify", "Pending in queue", "In progress"], + }) + public readonly result: string; +} diff --git a/packages/api/src/api/types.ts b/packages/api/src/api/types.ts index bb886dfaff..31dd9a91b1 100644 --- a/packages/api/src/api/types.ts +++ b/packages/api/src/api/types.ts @@ -26,8 +26,9 @@ export enum ApiAccountAction { export enum ApiContractAction { GetAbi = "getabi", GetSourceCode = "getsourcecode", - VerifySourceCode = "verifysourcecode", GetContractCreation = "getcontractcreation", + VerifySourceCode = "verifysourcecode", + GetVerificationStatus = "checkverifystatus", } export enum ApiTransactionAction { @@ -121,3 +122,8 @@ export enum ContractVerificationCodeFormatEnum { solidityJsonInput = "solidity-standard-json-input", vyperMultiFile = "vyper-multi-file", } + +export type ContractVerificationStatusResponse = { + status: "successful" | "failed" | "in_progress" | "queued"; + error?: string; +}; diff --git a/packages/api/test/contract-api.e2e-spec.ts b/packages/api/test/contract-api.e2e-spec.ts index 2d5a2bf2ff..67b20c348e 100644 --- a/packages/api/test/contract-api.e2e-spec.ts +++ b/packages/api/test/contract-api.e2e-spec.ts @@ -524,6 +524,98 @@ describe("Contract API (e2e)", () => { }); }); + describe("/api?module=contract&action=checkverifystatus GET", () => { + it("returns HTTP 200 and successful verification status", () => { + const verificationId = "1234"; + + nock(CONTRACT_VERIFICATION_API_URL).get(`/contract_verification/${verificationId}`).reply(200, { + status: "successful", + }); + + return request(app.getHttpServer()) + .get(`/api?module=contract&action=checkverifystatus&guid=${verificationId}`) + .expect(200) + .expect((res) => + expect(res.body).toStrictEqual({ + message: "OK", + result: "Pass - Verified", + status: "1", + }) + ); + }); + + it("returns HTTP 200 and queued verification status", () => { + const verificationId = "1234"; + + nock(CONTRACT_VERIFICATION_API_URL).get(`/contract_verification/${verificationId}`).reply(200, { + status: "queued", + }); + + return request(app.getHttpServer()) + .get(`/api?module=contract&action=checkverifystatus&guid=${verificationId}`) + .expect(200) + .expect((res) => + expect(res.body).toStrictEqual({ + message: "OK", + result: "Pending in queue", + status: "1", + }) + ); + }); + + it("returns HTTP 200 and in progress verification status", () => { + const verificationId = "1234"; + + nock(CONTRACT_VERIFICATION_API_URL).get(`/contract_verification/${verificationId}`).reply(200, { + status: "in_progress", + }); + + return request(app.getHttpServer()) + .get(`/api?module=contract&action=checkverifystatus&guid=${verificationId}`) + .expect(200) + .expect((res) => + expect(res.body).toStrictEqual({ + message: "OK", + result: "In progress", + status: "1", + }) + ); + }); + + it("returns HTTP 200 and in progress failed status", () => { + const verificationId = "1234"; + + nock(CONTRACT_VERIFICATION_API_URL).get(`/contract_verification/${verificationId}`).reply(200, { + status: "failed", + error: "ERROR! Compilation error.", + }); + + return request(app.getHttpServer()) + .get(`/api?module=contract&action=checkverifystatus&guid=${verificationId}`) + .expect(200) + .expect((res) => + expect(res.body).toStrictEqual({ + message: "NOTOK", + result: "ERROR! Compilation error.", + status: "0", + }) + ); + }); + + it("returns HTTP 200 and not OK if verification id is not valid", () => { + return request(app.getHttpServer()) + .get(`/api?module=contract&action=checkverifystatus`) + .expect(200) + .expect((res) => + expect(res.body).toStrictEqual({ + message: "NOTOK", + result: "Verification ID is not specified", + status: "0", + }) + ); + }); + }); + describe("/api?module=contract&action=getcontractcreation GET", () => { it("returns HTTP 200 and contract creation info when contract is found in DB", () => { const address = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";