Skip to content

Commit

Permalink
feat: etherscan like api to get contract source code (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasyl-ivanchuk authored Oct 2, 2023
1 parent d38a297 commit c33feff
Show file tree
Hide file tree
Showing 7 changed files with 882 additions and 2 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 @@ -41,6 +41,13 @@ describe("ApiController", () => {
});
});

describe("getContractSourceCode", () => {
it("returns null as it is defined only to appear in docs and cannot be called", async () => {
const result = await controller.getContractSourceCode();
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
17 changes: 17 additions & 0 deletions packages/api/src/api/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Request, NextFunction } from "express";
import { PagingOptionsWithMaxItemsLimitDto } from "./dtos/common/pagingOptionsWithMaxItemsLimit.dto";
import { ContractAbiResponseDto } from "./dtos/contract/contractAbiResponse.dto";
import { ContractCreationResponseDto, ContractCreationInfoDto } from "./dtos/contract/contractCreationResponse.dto";
import { ContractSourceCodeResponseDto } from "./dtos/contract/contractSourceCodeResponse.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 @@ -63,6 +64,22 @@ export class ApiController {
return null;
}

@ApiTags("Contract API")
@Get("api?module=contract&action=getsourcecode")
@ApiQuery({
name: "address",
description: "The contract address that has a verified source code",
example: "0x8A63F953e19aA4Ce3ED90621EeF61E17A95c6594",
required: true,
})
@ApiOkResponse({
description: "Contract source code",
type: ContractSourceCodeResponseDto,
})
public async getContractSourceCode(): Promise<ContractSourceCodeResponseDto> {
return null;
}

@ApiTags("Contract API")
@Get("api?module=contract&action=getcontractcreation")
@ApiQuery({
Expand Down
322 changes: 322 additions & 0 deletions packages/api/src/api/contract/contract.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,328 @@ describe("ContractController", () => {
});
});

describe("getContractSourceCode", () => {
let pipeMock = jest.fn();

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 info API fails with a server error", async () => {
pipeMock.mockImplementation((callback) => {
callback({
stack: "error stack",
response: {
data: "response data",
status: 500,
},
} as AxiosError);
});

await expect(controller.getContractSourceCode(address)).rejects.toThrowError(
new InternalServerErrorException("Failed to get contract source code")
);
});

it("returns empty source code when response API fails with with no data", async () => {
pipeMock.mockImplementation((callback) => {
return callback({
stack: "error stack",
} as AxiosError);
});

const response = await controller.getContractSourceCode(address);
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 empty source code response when contract verification info API fails with response status 404", async () => {
pipeMock.mockImplementation((callback) => {
return callback({
stack: "error stack",
response: {
data: "response data",
status: 404,
},
} as AxiosError);
});

const response = await controller.getContractSourceCode(address);
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",
});
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 = [];

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",
},
},
});
})
);

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(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: "",
},
],
status: "1",
});
expect(httpServiceMock.get).toBeCalledWith(`http://verification.api/contract_verification/info/${address}`);
});
});

describe("getContractCreation", () => {
it("thrown an error when called with more than 5 addresses", async () => {
await expect(
Expand Down
Loading

0 comments on commit c33feff

Please sign in to comment.