Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 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
5 changes: 5 additions & 0 deletions .changeset/famous-parts-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openzeppelin/wizard': patch
---

Add compatible git commit in comments when importing OpenZeppelin Community Contracts
9 changes: 9 additions & 0 deletions packages/core/solidity/openzeppelin-contracts.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
export interface OpenZeppelinContracts {
/**
* Version of `@openzeppelin/contracts` and `@openzeppelin/contracts-upgradeable`
*/
version: string;
/**
* Map of source file path to source code.
*/
sources: Record<string, string>;
/**
* Map of source file path to the list of all source file paths it depends on (including transitive dependencies).
*/
dependencies: Record<string, string[]>;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/solidity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"update-env": "rm ./src/environments/hardhat/package-lock.json && npm install --package-lock-only --prefix ./src/environments/hardhat && rm ./src/environments/hardhat/upgradeable/package-lock.json && npm install --package-lock-only --prefix ./src/environments/hardhat/upgradeable"
},
"devDependencies": {
"@openzeppelin/community-contracts": "https://github.com/OpenZeppelin/openzeppelin-community-contracts",
"@openzeppelin/community-contracts": "git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#de17c8e",
"@openzeppelin/contracts": "^5.4.0",
"@openzeppelin/contracts-upgradeable": "^5.4.0",
"@types/node": "^20.0.0",
Expand Down
154 changes: 77 additions & 77 deletions packages/core/solidity/src/account.test.ts.md

Large diffs are not rendered by default.

Binary file modified packages/core/solidity/src/account.test.ts.snap
Binary file not shown.
17 changes: 16 additions & 1 deletion packages/core/solidity/src/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import SOLIDITY_VERSION from './solidity-version.json';
import { inferTranspiled } from './infer-transpiled';
import { compatibleContractsSemver } from './utils/version';
import { stringifyUnicodeSafe } from './utils/sanitize';
import { importsCommunityContracts } from './utils/imports-libraries';
import { getCommunityContractsGitCommit } from './utils/community-contracts-git-commit';

export function printContract(contract: Contract, opts?: Options): string {
const helpers = withHelpers(contract, opts);
Expand All @@ -29,7 +31,7 @@ export function printContract(contract: Contract, opts?: Options): string {
...spaceBetween(
[
`// SPDX-License-Identifier: ${contract.license}`,
`// Compatible with OpenZeppelin Contracts ${compatibleContractsSemver}`,
printCompatibleLibraryVersions(contract),
`pragma solidity ^${SOLIDITY_VERSION};`,
],

Expand All @@ -54,6 +56,19 @@ export function printContract(contract: Contract, opts?: Options): string {
);
}

function printCompatibleLibraryVersions(contract: Contract): string {
let result = `// Compatible with OpenZeppelin Contracts ${compatibleContractsSemver}`;
if (importsCommunityContracts(contract)) {
try {
const commit = getCommunityContractsGitCommit();
result += ` and Community Contracts commit ${commit}`;
} catch (e) {
console.error(e);
}
}
return result;
}

function printInheritance(contract: Contract, { transformName }: Helpers): [] | [string] {
if (contract.parents.length > 0) {
return ['is ' + contract.parents.map(p => transformName(p.contract)).join(', ')];
Expand Down
8 changes: 4 additions & 4 deletions packages/core/solidity/src/stablecoin.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.4.0␊
// Compatible with OpenZeppelin Contracts ^5.4.0 and Community Contracts commit de17c8e
pragma solidity ^0.8.27;␊
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
Expand Down Expand Up @@ -362,7 +362,7 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.4.0␊
// Compatible with OpenZeppelin Contracts ^5.4.0 and Community Contracts commit de17c8e
pragma solidity ^0.8.27;␊
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
Expand Down Expand Up @@ -408,7 +408,7 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.4.0␊
// Compatible with OpenZeppelin Contracts ^5.4.0 and Community Contracts commit de17c8e
pragma solidity ^0.8.27;␊
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
Expand Down Expand Up @@ -588,7 +588,7 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.4.0␊
// Compatible with OpenZeppelin Contracts ^5.4.0 and Community Contracts commit de17c8e
pragma solidity ^0.8.27;␊
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";␊
Expand Down
Binary file modified packages/core/solidity/src/stablecoin.test.ts.snap
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import test from 'ava';

import { extractGitCommitHash } from './community-contracts-git-commit';

// Valid cases

test('extractGitCommitHash - lowercase 40-char hash', t => {
const hash = extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#0123456789abcdef0123456789abcdef01234567',
);
t.is(hash, '0123456789abcdef0123456789abcdef01234567');
});

test('extractGitCommitHash - uppercase 7-char hash returns lowercase', t => {
const hash = extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+ssh://[email protected]/OpenZeppelin/openzeppelin-community-contracts.git#ABCDEF1',
);
t.is(hash, 'abcdef1');
});

// Invalid format cases

test('extractGitCommitHash - missing git+ prefix', t => {
const err = t.throws(() =>
extractGitCommitHash(
'@openzeppelin/community-contracts',
'https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#abcdef1',
),
);
t.true(
err instanceof Error &&
err.message.includes(
'Expected package dependency for @openzeppelin/community-contracts in format git+<url>#<commit-hash>,',
),
);
});

test('extractGitCommitHash - missing #', t => {
const err = t.throws(() =>
extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git',
),
);
t.true(
err instanceof Error &&
err.message.includes(
'Expected package dependency for @openzeppelin/community-contracts in format git+<url>#<commit-hash>,',
),
);
});

test('extractGitCommitHash - multiple # parts', t => {
const err = t.throws(() =>
extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#abcdef1#extra',
),
);
t.true(
err instanceof Error &&
err.message.includes(
'Expected package dependency for @openzeppelin/community-contracts in format git+<url>#<commit-hash>,',
),
);
});

// Invalid hash content cases

test('extractGitCommitHash - too short hash', t => {
const err = t.throws(() =>
extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#abcde',
),
);
t.true(
err instanceof Error &&
err.message.includes(
'Expected git commit hash for package dependency @openzeppelin/community-contracts to have between 7 and 40 hex chars',
),
);
});

test('extractGitCommitHash - too long hash', t => {
const err = t.throws(() =>
extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#0123456789abcdef0123456789abcdef012345678',
),
);
t.true(
err instanceof Error &&
err.message.includes(
'Expected git commit hash for package dependency @openzeppelin/community-contracts to have between 7 and 40 hex chars',
),
);
});

test('extractGitCommitHash - non-hex characters', t => {
const err = t.throws(() =>
extractGitCommitHash(
'@openzeppelin/community-contracts',
'git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#abcdefg',
),
);
t.true(
err instanceof Error &&
err.message.includes(
'Expected git commit hash for package dependency @openzeppelin/community-contracts to have between 7 and 40 hex chars',
),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { devDependencies } from '../../package.json';

/**
* @returns The git commit hash of the @openzeppelin/community-contracts package dependency.
* @throws Error if the @openzeppelin/community-contracts package dependency is not found in devDependencies.
*/
export function getCommunityContractsGitCommit(): string {
const communityContractsVersion = devDependencies['@openzeppelin/community-contracts'];
if (!communityContractsVersion) {
throw new Error('@openzeppelin/community-contracts not found in devDependencies');
}
return extractGitCommitHash('@openzeppelin/community-contracts', communityContractsVersion);
}

/**
* Extracts the git commit hash from a package dependency version string.
* The expected format is `git+<url>#<commit-hash>`.
*
* @param dependencyName The name of the package dependency.
* @param dependencyVersion The version string of the package dependency.
* @returns The git commit hash.
* @throws Error if the version string or commit hash is not in the expected format.
*/
export function extractGitCommitHash(dependencyName: string, dependencyVersion: string): string {
const split = dependencyVersion.split('#');
if (!dependencyVersion.startsWith('git+') || split.length !== 2) {
throw new Error(
`Expected package dependency for ${dependencyName} in format git+<url>#<commit-hash>, but got ${dependencyVersion}`,
);
}
const hash = split[1]!;
if (!/^[a-fA-F0-9]{7,40}$/.test(hash)) {
throw new Error(
`Expected git commit hash for package dependency ${dependencyName} to have between 7 and 40 hex chars, but got ${hash}`,
);
}
return hash.toLowerCase();
}
5 changes: 5 additions & 0 deletions packages/core/solidity/src/utils/imports-libraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Contract } from '../contract';

export function importsCommunityContracts(contract: Contract) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits: as it returns a boolean maybe could be named doesImportsCommunityContracts

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The s in imports implies boolean

return contract.imports.some(i => i.path.startsWith('@openzeppelin/community-contracts/'));
}
37 changes: 26 additions & 11 deletions packages/ui/src/solidity/inject-hyperlinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,32 @@ import { version as contractsVersion } from '@openzeppelin/contracts/package.jso

export function injectHyperlinks(code: string) {
// We are modifying HTML, so use HTML escaped chars. The pattern excludes paths that include /../ in the URL.
const contractsRegex =
const importContractsRegex =
/&quot;(@openzeppelin\/)(contracts-upgradeable\/|contracts\/)((?:(?!\.\.)[^/]+\/)*?[^/]*?)&quot;/g;
const communityContractsRegex = /&quot;(@openzeppelin\/)(community-contracts\/)((?:(?!\.\.)[^/]+\/)*?[^/]*?)&quot;/g;
const importCommunityContractsRegex =
/&quot;(@openzeppelin\/)(community-contracts\/)((?:(?!\.\.)[^/]+\/)*?[^/]*?)&quot;/g;

return code
.replace(
contractsRegex,
`&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-$2blob/v${contractsVersion}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
)
.replace(
communityContractsRegex,
`&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/master/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
);
const compatibleCommunityContractsRegexSingle = /Community Contracts commit ([a-fA-F0-9]{7,40})/;
const compatibleCommunityContractsRegexGlobal = new RegExp(compatibleCommunityContractsRegexSingle.source, 'g');

const compatibleCommunityContractsGitCommit = code.match(compatibleCommunityContractsRegexSingle)?.[1];

let result = code.replace(
importContractsRegex,
`&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-$2blob/v${contractsVersion}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
);

if (compatibleCommunityContractsGitCommit !== undefined) {
result = result
.replace(
importCommunityContractsRegex,
`&quot;<a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/${compatibleCommunityContractsGitCommit}/contracts/$3" target="_blank" rel="noopener noreferrer">$1$2$3</a>&quot;`,
)
.replace(
compatibleCommunityContractsRegexGlobal,
`Community Contracts commit <a class="import-link" href="https://github.com/OpenZeppelin/openzeppelin-community-contracts/tree/$1" target="_blank" rel="noopener noreferrer" title="View repository at commit $1">$1</a>`,
);
}

return result;
}
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -823,9 +823,9 @@
"@nomicfoundation/solidity-analyzer-linux-x64-musl" "0.1.2"
"@nomicfoundation/solidity-analyzer-win32-x64-msvc" "0.1.2"

"@openzeppelin/community-contracts@https://github.com/OpenZeppelin/openzeppelin-community-contracts":
"@openzeppelin/community-contracts@git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#de17c8e":
version "0.0.1"
resolved "https://github.com/OpenZeppelin/openzeppelin-community-contracts#de17c8ee4b0329867f7219fbc401707be9518ff1"
resolved "git+https://github.com/OpenZeppelin/openzeppelin-community-contracts.git#de17c8ee4b0329867f7219fbc401707be9518ff1"

"@openzeppelin/contracts-upgradeable@^5.4.0":
version "5.4.0"
Expand Down
Loading