Skip to content

Commit

Permalink
feat: get verification status endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
vasyl-ivanchuk committed Oct 27, 2023
1 parent b731749 commit fe4cedb
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 15 deletions.
7 changes: 7 additions & 0 deletions packages/api/src/api/api.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
40 changes: 29 additions & 11 deletions packages/api/src/api/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<ContractCreationResponseDto> {
return null;
}

@ApiTags("Contract API")
@Post("api")
@ApiOperation({ summary: "Submits a contract source code for verification" })
Expand All @@ -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<ContractCreationResponseDto> {
public async getVerificationStatus(): Promise<ContractVerificationStatusResponseDto> {
return null;
}

Expand Down
154 changes: 153 additions & 1 deletion packages/api/src/api/contract/contract.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<AxiosResponse>);
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(
Expand Down
64 changes: 62 additions & 2 deletions packages/api/src/api/contract/contract.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -233,4 +238,59 @@ export class ContractController {
result: result.length ? result : null,
};
}

@Get("/checkverifystatus")
public async getVerificationStatus(
@Query("guid") verificationId: string
): Promise<ContractVerificationStatusResponseDto> {
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,
};
}
}
3 changes: 3 additions & 0 deletions packages/api/src/api/dtos/common/responseBase.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 7 additions & 1 deletion packages/api/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
};
Loading

0 comments on commit fe4cedb

Please sign in to comment.