Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/lib-sourcify/src/utils/etherscan/etherscan-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ interface VyperVersionCache {
let vyperVersionCache: VyperVersionCache | null = null;
const CACHE_DURATION_MS = 60 * 60 * 1000; // 1 hour default

// Etherscan encodes early Vyper betas as e.g. "0.1.0b17" while the GitHub release
// tag (and the Hardhat mirror) uses "0.1.0-beta.17". Normalize before lookup.
const normalizeVyperVersion = (v: string): string =>
v.replace(/^(\d+\.\d+\.\d+)b(\d+)$/, '$1-beta.$2');

export const getVyperCompilerVersion = async (
compilerString: string,
cacheDurationMs: number = CACHE_DURATION_MS,
Expand Down Expand Up @@ -64,8 +69,9 @@ export const getVyperCompilerVersion = async (

if (!vyperVersionCache) return undefined;
const versionNumber = compilerString.split(':')[1];
const normalizedVersion = normalizeVyperVersion(versionNumber);
let found = vyperVersionCache.versions.find(
(v) => v.tag === versionNumber,
(v) => v.tag === versionNumber || v.tag === normalizedVersion,
)?.compiler_version;
if (!found) {
// If not found, try a one-time refresh to capture newly added versions
Expand All @@ -86,7 +92,7 @@ export const getVyperCompilerVersion = async (
lastFetch: Date.now(),
};
found = vyperVersionCache.versions.find(
(v) => v.tag === versionNumber,
(v) => v.tag === versionNumber || v.tag === normalizedVersion,
)?.compiler_version;
} catch (error) {
logWarn('Failed to refresh Vyper versions for missing tag', {
Expand Down
92 changes: 92 additions & 0 deletions packages/lib-sourcify/test/utils/etherscan-util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { solc, vyperCompiler } from '../utils';
// and ensure consistency across lib-sourcify and server tests.
import {
INVALID_API_KEY_RESPONSE,
MALFORMED_VYPER_VERSION_RESPONSE,
MULTIPLE_CONTRACT_RESPONSE,
RATE_LIMIT_REACHED_RESPONSE,
SINGLE_CONTRACT_RESPONSE,
Expand Down Expand Up @@ -453,4 +454,95 @@ describe('etherscan util (lib)', function () {
);
});
});

describe('getVyperCompilerVersion', () => {
it('should resolve "vyper:0.1.0b17" to the GitHub tag format', async () => {
nock('https://vyper-releases-mirror.hardhat.org')
.get('/list.json')
.times(2)
.reply(200, [
{
tag_name: 'v0.1.0-beta.17',
assets: [{ name: 'vyper.0.1.0-beta.17+commit.0671b7b.darwin' }],
},
]);

const result = await EtherscanUtils.getVyperCompilerVersion(
'vyper:0.1.0b17',
0,
);
expect(result).to.equal('0.1.0-beta.17+commit.0671b7b');
});

it('should resolve "vyper:0.1.0b16" to the GitHub tag format', async () => {
nock('https://vyper-releases-mirror.hardhat.org')
.get('/list.json')
.times(2)
.reply(200, [
{
tag_name: 'v0.1.0-beta.16',
assets: [{ name: 'vyper.0.1.0-beta.16+commit.5e4a94a.darwin' }],
},
]);

const result = await EtherscanUtils.getVyperCompilerVersion(
'vyper:0.1.0b16',
0,
);
expect(result).to.equal('0.1.0-beta.16+commit.5e4a94a');
});

it('should still resolve a well-formed "vyper:0.3.10"', async () => {
nock('https://vyper-releases-mirror.hardhat.org')
.get('/list.json')
.times(2)
.reply(200, [
{
tag_name: 'v0.3.10',
assets: [{ name: 'vyper.0.3.10+commit.91361694.darwin' }],
},
]);

const result = await EtherscanUtils.getVyperCompilerVersion(
'vyper:0.3.10',
0,
);
expect(result).to.equal('0.3.10+commit.91361694');
});

it('should return undefined for a version not in the mirror', async () => {
nock('https://vyper-releases-mirror.hardhat.org')
.get('/list.json')
.times(2)
.reply(200, [
{
tag_name: 'v0.1.0-beta.17',
assets: [{ name: 'vyper.0.1.0-beta.17+commit.0671b7b.darwin' }],
},
]);

const result = await EtherscanUtils.getVyperCompilerVersion(
'vyper:0.1.0b4',
0,
);
expect(result).to.be.undefined;
});

it('should resolve a malformed Vyper version via processVyperResultFromEtherscan', async () => {
nock('https://vyper-releases-mirror.hardhat.org')
.get('/list.json')
.times(2)
.reply(200, [
{
tag_name: 'v0.1.0-beta.17',
assets: [{ name: 'vyper.0.1.0-beta.17+commit.0671b7b.darwin' }],
},
]);

const result = await EtherscanUtils.processVyperResultFromEtherscan(
MALFORMED_VYPER_VERSION_RESPONSE.result[0] as any,
);
expect(result.compilerVersion).to.equal('0.1.0-beta.17+commit.0671b7b');
});
});
});
25 changes: 25 additions & 0 deletions services/server/test/helpers/etherscanResponseMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,28 @@ export const VYPER_STANDARD_JSON_CONTRACT_RESPONSE = {
},
],
};

// Same shape as VYPER_SINGLE_CONTRACT_RESPONSE but with the old Etherscan beta format
// "vyper:0.1.0b17" instead of the normalized GitHub tag "0.1.0-beta.17".
export const MALFORMED_VYPER_VERSION_RESPONSE = {
status: "1",
message: "OK",
result: [
{
SourceCode:
"# @version 0.1.0b17\r\n# A minimal Vyper contract for testing version normalization\r\n\r\nowner: address\r\n\r\n@deploy\r\ndef __init__():\r\n self.owner = msg.sender",
ABI: "[]",
ContractName: "SimpleOwner",
CompilerVersion: "vyper:0.1.0b17",
OptimizationUsed: "0",
Runs: "0",
ConstructorArguments: "",
EVMVersion: "Default",
Library: "",
LicenseType: "MIT",
Proxy: "0",
Implementation: "",
SwarmSource: "",
},
],
};