Skip to content

Commit a6324d1

Browse files
authored
Merge pull request #4695 from Chlebamaticon/feat-4652-viem-library-linking
issues/4652: added support for library linking in `hardhat-viem`
2 parents 8c9106b + 581ae2b commit a6324d1

File tree

9 files changed

+510
-11
lines changed

9 files changed

+510
-11
lines changed

.changeset/dry-jobs-fail.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/hardhat-viem": patch
3+
---
4+
5+
Added support for library linking (thanks @Chlebamaticon!)

packages/hardhat-viem/README.md

+20
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,26 @@ const { contractAddress } = await publicClient.waitForTransactionReceipt({
228228
});
229229
```
230230

231+
##### Library linking
232+
233+
Some contracts need to be linked with libraries before they are deployed. You can pass the addresses of their libraries to the `deployContract` and `sendDeploymentTransaction` functions with an object like this:
234+
235+
```typescript
236+
const contractA = await hre.viem.deployContract(
237+
"contractName",
238+
["arg1", 50, "arg3"],
239+
{
240+
libraries: {
241+
ExampleLib: "0x...",
242+
},
243+
}
244+
);
245+
```
246+
247+
This allows you to deploy a contract linked to the `ExampleLib` library at the address `"0x..."`.
248+
249+
To deploy a contract, all libraries must be linked. An error will be thrown if any libraries are missing.
250+
231251
## Usage
232252

233253
There are no additional steps you need to take for this plugin to work.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type * as viemT from "viem";
2+
import type { Artifact } from "hardhat/types/artifacts";
3+
4+
import {
5+
AmbigousLibraryNameError,
6+
MissingLibraryAddressError,
7+
OverlappingLibraryNamesError,
8+
UnnecessaryLibraryLinkError,
9+
} from "./errors";
10+
11+
export interface Libraries<Address = string> {
12+
[libraryName: string]: Address;
13+
}
14+
15+
export interface Link {
16+
sourceName: string;
17+
libraryName: string;
18+
address: string;
19+
}
20+
21+
export async function linkBytecode(
22+
artifact: Artifact,
23+
libraries: Link[]
24+
): Promise<viemT.Hex> {
25+
const { isHex } = await import("viem");
26+
let bytecode = artifact.bytecode;
27+
28+
// TODO: measure performance impact
29+
for (const { sourceName, libraryName, address } of libraries) {
30+
const linkReferences = artifact.linkReferences[sourceName][libraryName];
31+
for (const { start, length } of linkReferences) {
32+
bytecode =
33+
bytecode.substring(0, 2 + start * 2) +
34+
address.substring(2) +
35+
bytecode.substring(2 + (start + length) * 2);
36+
}
37+
}
38+
39+
return isHex(bytecode) ? bytecode : `0x${bytecode}`;
40+
}
41+
42+
async function throwOnAmbigousLibraryNameOrUnnecessaryLink(
43+
contractName: string,
44+
libraries: Libraries<viemT.Address>,
45+
neededLibraries: Link[]
46+
) {
47+
for (const linkedLibraryName of Object.keys(libraries)) {
48+
const matchingLibraries = neededLibraries.filter(
49+
({ sourceName, libraryName }) =>
50+
libraryName === linkedLibraryName ||
51+
`${sourceName}:${libraryName}` === linkedLibraryName
52+
);
53+
54+
if (matchingLibraries.length > 1) {
55+
throw new AmbigousLibraryNameError(
56+
contractName,
57+
linkedLibraryName,
58+
matchingLibraries.map(
59+
({ sourceName, libraryName }) => `${sourceName}:${libraryName}`
60+
)
61+
);
62+
} else if (matchingLibraries.length === 0) {
63+
throw new UnnecessaryLibraryLinkError(contractName, linkedLibraryName);
64+
}
65+
}
66+
}
67+
68+
async function throwOnMissingLibrariesAddress(
69+
contractName: string,
70+
libraries: Libraries<viemT.Address>,
71+
neededLibraries: Link[]
72+
) {
73+
const missingLibraries = [];
74+
for (const { sourceName, libraryName } of neededLibraries) {
75+
const address =
76+
libraries[`${sourceName}:${libraryName}`] ?? libraries[libraryName];
77+
78+
if (address === undefined) {
79+
missingLibraries.push({ sourceName, libraryName });
80+
}
81+
}
82+
83+
if (missingLibraries.length > 0) {
84+
throw new MissingLibraryAddressError(contractName, missingLibraries);
85+
}
86+
}
87+
88+
async function throwOnOverlappingLibraryNames(
89+
contractName: string,
90+
libraries: Libraries<viemT.Address>,
91+
neededLibraries: Link[]
92+
) {
93+
for (const { sourceName, libraryName } of neededLibraries) {
94+
if (
95+
libraries[`${sourceName}:${libraryName}`] !== undefined &&
96+
libraries[libraryName] !== undefined
97+
) {
98+
throw new OverlappingLibraryNamesError(sourceName, libraryName);
99+
}
100+
}
101+
}
102+
103+
export async function resolveBytecodeWithLinkedLibraries(
104+
artifact: Artifact,
105+
libraries: Libraries<viemT.Address>
106+
): Promise<viemT.Hex> {
107+
const { linkReferences } = artifact;
108+
109+
const neededLibraries: Link[] = [];
110+
for (const [sourceName, sourceLibraries] of Object.entries(linkReferences)) {
111+
for (const libraryName of Object.keys(sourceLibraries)) {
112+
neededLibraries.push({
113+
sourceName,
114+
libraryName,
115+
address:
116+
libraries[`${sourceName}:${libraryName}`] ?? libraries[libraryName],
117+
});
118+
}
119+
}
120+
121+
await throwOnAmbigousLibraryNameOrUnnecessaryLink(
122+
artifact.contractName,
123+
libraries,
124+
neededLibraries
125+
);
126+
await throwOnOverlappingLibraryNames(
127+
artifact.contractName,
128+
libraries,
129+
neededLibraries
130+
);
131+
await throwOnMissingLibrariesAddress(
132+
artifact.contractName,
133+
libraries,
134+
neededLibraries
135+
);
136+
137+
return linkBytecode(artifact, neededLibraries);
138+
}

packages/hardhat-viem/src/internal/contracts.ts

+33-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
WalletClient,
1414
} from "../types";
1515

16+
import { Libraries, resolveBytecodeWithLinkedLibraries } from "./bytecode";
1617
import { getPublicClient, getWalletClients } from "./clients";
1718
import {
1819
DefaultWalletClientNotFoundError,
@@ -21,24 +22,46 @@ import {
2122
InvalidConfirmationsError,
2223
} from "./errors";
2324

25+
async function getContractAbiAndBytecode(
26+
artifacts: HardhatRuntimeEnvironment["artifacts"],
27+
contractName: string,
28+
libraries: Libraries<Address>
29+
) {
30+
const artifact = await artifacts.readArtifact(contractName);
31+
const bytecode = await resolveBytecodeWithLinkedLibraries(
32+
artifact,
33+
libraries
34+
);
35+
36+
return {
37+
abi: artifact.abi,
38+
bytecode,
39+
};
40+
}
41+
2442
export async function deployContract(
2543
{ artifacts, network }: HardhatRuntimeEnvironment,
2644
contractName: string,
2745
constructorArgs: any[] = [],
2846
config: DeployContractConfig = {}
2947
): Promise<GetContractReturnType> {
30-
const { client, confirmations, ...deployContractParameters } = config;
31-
const [publicClient, walletClient, contractArtifact] = await Promise.all([
48+
const {
49+
client,
50+
confirmations,
51+
libraries = {},
52+
...deployContractParameters
53+
} = config;
54+
const [publicClient, walletClient, { abi, bytecode }] = await Promise.all([
3255
client?.public ?? getPublicClient(network.provider),
3356
client?.wallet ?? getDefaultWalletClient(network.provider, network.name),
34-
artifacts.readArtifact(contractName),
57+
getContractAbiAndBytecode(artifacts, contractName, libraries),
3558
]);
3659

3760
return innerDeployContract(
3861
publicClient,
3962
walletClient,
40-
contractArtifact.abi,
41-
contractArtifact.bytecode as Hex,
63+
abi,
64+
bytecode,
4265
constructorArgs,
4366
deployContractParameters,
4467
confirmations
@@ -114,18 +137,18 @@ export async function sendDeploymentTransaction(
114137
contract: GetContractReturnType;
115138
deploymentTransaction: GetTransactionReturnType;
116139
}> {
117-
const { client, ...deployContractParameters } = config;
118-
const [publicClient, walletClient, contractArtifact] = await Promise.all([
140+
const { client, libraries = {}, ...deployContractParameters } = config;
141+
const [publicClient, walletClient, { abi, bytecode }] = await Promise.all([
119142
client?.public ?? getPublicClient(network.provider),
120143
client?.wallet ?? getDefaultWalletClient(network.provider, network.name),
121-
artifacts.readArtifact(contractName),
144+
getContractAbiAndBytecode(artifacts, contractName, libraries),
122145
]);
123146

124147
return innerSendDeploymentTransaction(
125148
publicClient,
126149
walletClient,
127-
contractArtifact.abi,
128-
contractArtifact.bytecode as Hex,
150+
abi,
151+
bytecode,
129152
constructorArgs,
130153
deployContractParameters
131154
);

packages/hardhat-viem/src/internal/errors.ts

+49
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Link } from "./bytecode";
2+
13
import { NomicLabsHardhatPluginError } from "hardhat/plugins";
24

35
export class HardhatViemError extends NomicLabsHardhatPluginError {
@@ -74,3 +76,50 @@ export class DeployContractError extends HardhatViemError {
7476
);
7577
}
7678
}
79+
80+
export class AmbigousLibraryNameError extends HardhatViemError {
81+
constructor(
82+
contractName: string,
83+
libraryName: string,
84+
matchingLibraries: string[]
85+
) {
86+
super(
87+
`The library name "${libraryName}" is ambiguous for the contract "${contractName}".
88+
It may resolve to one of the following libraries:
89+
${matchingLibraries.map((fqn) => `\n\t* ${fqn}`).join(",")}
90+
91+
To fix this, choose one of these fully qualified library names and replace where appropriate.`
92+
);
93+
}
94+
}
95+
96+
export class OverlappingLibraryNamesError extends HardhatViemError {
97+
constructor(sourceName: string, libraryName: string) {
98+
super(
99+
`The library name "${libraryName}" and "${sourceName}:${libraryName}" are both linking to the same library. Please use one of them, or If they are not the same library, use fully qualified names instead.`
100+
);
101+
}
102+
}
103+
104+
export class UnnecessaryLibraryLinkError extends HardhatViemError {
105+
constructor(contractName: string, libraryName: string) {
106+
super(
107+
`The library name "${libraryName}" was linked but it's not referenced by the "${contractName}" contract.`
108+
);
109+
}
110+
}
111+
112+
export class MissingLibraryAddressError extends HardhatViemError {
113+
constructor(
114+
contractName: string,
115+
missingLibraries: Array<Pick<Link, "sourceName" | "libraryName">>
116+
) {
117+
super(
118+
`The libraries needed are:
119+
${missingLibraries
120+
.map(({ sourceName, libraryName }) => `\t* "${sourceName}:${libraryName}"`)
121+
.join(",\n")}
122+
Please deploy them first and link them while deploying "${contractName}"`
123+
);
124+
}
125+
}

packages/hardhat-viem/src/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type * as viemT from "viem";
22
import type { ArtifactsMap } from "hardhat/types/artifacts";
3+
import type { Libraries } from "./internal/bytecode";
34

45
export type PublicClient = viemT.PublicClient<viemT.Transport, viemT.Chain>;
56
export type WalletClient = viemT.WalletClient<
@@ -38,9 +39,12 @@ export interface SendTransactionConfig {
3839

3940
export interface DeployContractConfig extends SendTransactionConfig {
4041
confirmations?: number;
42+
libraries?: Libraries<viemT.Address>;
4143
}
4244

43-
export type SendDeploymentTransactionConfig = SendTransactionConfig;
45+
export interface SendDeploymentTransactionConfig extends SendTransactionConfig {
46+
libraries?: Libraries<viemT.Address>;
47+
}
4448

4549
export interface GetContractAtConfig {
4650
client?: KeyedClient;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.19;
3+
4+
library ConstructorLib {
5+
function libDo(uint256 n) pure external returns (uint256) {
6+
return n * n;
7+
}
8+
}

0 commit comments

Comments
 (0)