Skip to content

Commit 7ccf081

Browse files
authored
Merge pull request #6225 from sourcifyeth/sourcify-apiv2
Verification Plugin: Use Sourcify API v2
2 parents 6a9c5fb + 283a76e commit 7ccf081

File tree

11 files changed

+373
-103
lines changed

11 files changed

+373
-103
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { CompilerAbstract, SourcesCode } from '@remix-project/remix-solidity'
2+
import { AbstractVerifier } from './AbstractVerifier'
3+
import type { LookupResponse, SourceFile, SubmittedContract, VerificationResponse, VerificationStatus } from '../types'
4+
import { getAddress } from 'ethers'
5+
6+
interface SourcifyV1VerificationRequest {
7+
address: string
8+
chain: string
9+
files: Record<string, string>
10+
creatorTxHash?: string
11+
chosenContract?: string
12+
}
13+
14+
type SourcifyV1VerificationStatus = 'perfect' | 'full' | 'partial' | null
15+
16+
interface SourcifyV1VerificationResponse {
17+
result: [
18+
{
19+
address: string
20+
chainId: string
21+
status: SourcifyV1VerificationStatus
22+
libraryMap: {
23+
[key: string]: string
24+
}
25+
message?: string
26+
}
27+
]
28+
}
29+
30+
interface SourcifyV1ErrorResponse {
31+
error: string
32+
}
33+
34+
interface SourcifyV1File {
35+
name: string
36+
path: string
37+
content: string
38+
}
39+
40+
interface SourcifyV1LookupResponse {
41+
status: Exclude<SourcifyV1VerificationStatus, null>
42+
files: SourcifyV1File[]
43+
}
44+
45+
export class SourcifyV1Verifier extends AbstractVerifier {
46+
LOOKUP_STORE_DIR = 'sourcify-verified'
47+
48+
async verify(submittedContract: SubmittedContract, compilerAbstract: CompilerAbstract): Promise<VerificationResponse> {
49+
const metadataStr = compilerAbstract.data.contracts[submittedContract.filePath][submittedContract.contractName].metadata
50+
const sources = compilerAbstract.source.sources
51+
52+
// from { "filename.sol": {content: "contract MyContract { ... }"} }
53+
// to { "filename.sol": "contract MyContract { ... }" }
54+
const formattedSources = Object.entries(sources).reduce((acc, [fileName, { content }]) => {
55+
acc[fileName] = content
56+
return acc
57+
}, {})
58+
const body: SourcifyV1VerificationRequest = {
59+
chain: submittedContract.chainId,
60+
address: submittedContract.address,
61+
files: {
62+
'metadata.json': metadataStr,
63+
...formattedSources,
64+
},
65+
}
66+
67+
const response = await fetch(new URL(this.apiUrl + '/verify').href, {
68+
method: 'POST',
69+
headers: {
70+
'Content-Type': 'application/json',
71+
},
72+
body: JSON.stringify(body),
73+
})
74+
75+
if (!response.ok) {
76+
const errorResponse: SourcifyV1ErrorResponse = await response.json()
77+
console.error('Error on Sourcify verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + JSON.stringify(errorResponse))
78+
throw new Error(errorResponse.error)
79+
}
80+
81+
const verificationResponse: SourcifyV1VerificationResponse = await response.json()
82+
83+
if (verificationResponse.result[0].status === null) {
84+
console.error('Error on Sourcify verification at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + verificationResponse.result[0].message)
85+
throw new Error(verificationResponse.result[0].message)
86+
}
87+
88+
// Map to a user-facing status message
89+
let status: VerificationStatus = 'unknown'
90+
let lookupUrl: string | undefined = undefined
91+
if (verificationResponse.result[0].status === 'perfect' || verificationResponse.result[0].status === 'full') {
92+
status = 'exactly verified'
93+
lookupUrl = this.getContractCodeUrl(submittedContract.address, submittedContract.chainId, true)
94+
} else if (verificationResponse.result[0].status === 'partial') {
95+
status = 'verified'
96+
lookupUrl = this.getContractCodeUrl(submittedContract.address, submittedContract.chainId, false)
97+
}
98+
99+
return { status, receiptId: null, lookupUrl }
100+
}
101+
102+
async lookup(contractAddress: string, chainId: string): Promise<LookupResponse> {
103+
const url = new URL(this.apiUrl + `/files/any/${chainId}/${contractAddress}`)
104+
105+
const response = await fetch(url.href, { method: 'GET' })
106+
107+
if (!response.ok) {
108+
const errorResponse: SourcifyV1ErrorResponse = await response.json()
109+
110+
if (errorResponse.error === 'Files have not been found!') {
111+
return { status: 'not verified' }
112+
}
113+
114+
console.error('Error on Sourcify lookup at ' + this.apiUrl + '\nStatus: ' + response.status + '\nResponse: ' + JSON.stringify(errorResponse))
115+
throw new Error(errorResponse.error)
116+
}
117+
118+
const lookupResponse: SourcifyV1LookupResponse = await response.json()
119+
120+
let status: VerificationStatus = 'unknown'
121+
let lookupUrl: string | undefined = undefined
122+
if (lookupResponse.status === 'perfect' || lookupResponse.status === 'full') {
123+
status = 'exactly verified'
124+
lookupUrl = this.getContractCodeUrl(contractAddress, chainId, true)
125+
} else if (lookupResponse.status === 'partial') {
126+
status = 'verified'
127+
lookupUrl = this.getContractCodeUrl(contractAddress, chainId, false)
128+
}
129+
130+
const { sourceFiles, targetFilePath } = this.processReceivedFiles(lookupResponse.files, contractAddress, chainId)
131+
132+
return { status, lookupUrl, sourceFiles, targetFilePath }
133+
}
134+
135+
getContractCodeUrl(address: string, chainId: string, fullMatch: boolean): string {
136+
const url = new URL(this.explorerUrl + `/contracts/${fullMatch ? 'full_match' : 'partial_match'}/${chainId}/${address}/`)
137+
return url.href
138+
}
139+
140+
processReceivedFiles(files: SourcifyV1File[], contractAddress: string, chainId: string): { sourceFiles: SourceFile[]; targetFilePath?: string } {
141+
const result: SourceFile[] = []
142+
let targetFilePath: string
143+
const filePrefix = `/${this.LOOKUP_STORE_DIR}/${chainId}/${contractAddress}`
144+
145+
for (const file of files) {
146+
let filePath: string
147+
for (const a of [contractAddress, getAddress(contractAddress)]) {
148+
const matching = file.path.match(`/${a}/(.*)$`)
149+
if (matching) {
150+
filePath = matching[1]
151+
break
152+
}
153+
}
154+
155+
if (filePath) {
156+
result.push({ path: `${filePrefix}/${filePath}`, content: file.content })
157+
}
158+
159+
if (file.name === 'metadata.json') {
160+
const metadata = JSON.parse(file.content)
161+
const compilationTarget = metadata.settings.compilationTarget
162+
const contractPath = Object.keys(compilationTarget)[0]
163+
targetFilePath = `${filePrefix}/sources/${contractPath}`
164+
}
165+
}
166+
167+
return { sourceFiles: result, targetFilePath }
168+
}
169+
}

0 commit comments

Comments
 (0)