Skip to content

Commit aa8ccfa

Browse files
authored
feat: support local solidity imports (#29)
1 parent 25a9806 commit aa8ccfa

6 files changed

Lines changed: 57 additions & 11 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,7 @@ bun install
4141
```bash
4242
bun dev
4343
```
44+
45+
## Deploying Contracts with Local Imports
46+
47+
Web3GPT now supports deploying factory contracts that rely on local Solidity imports. Provide additional source files alongside your main contract and reference them with relative paths (e.g., `import "./AddressBook.sol";`). The compiler will include these dependencies automatically, enabling factory patterns without flattening contracts.

lib/actions/deploy-contract.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ import { withUser } from "@/lib/data/kv"
88
import { getContractFileName, prepareContractSources } from "@/lib/solidity/utils"
99
import { ensureHashPrefix } from "@/lib/utils"
1010

11-
export async function compileContract({ contractName, sourceCode }: { contractName: string; sourceCode: string }) {
12-
const sources = await prepareContractSources(contractName, sourceCode)
11+
export async function compileContract({
12+
contractName,
13+
sourceCode,
14+
sources: additionalSources,
15+
}: {
16+
contractName: string
17+
sourceCode: string
18+
sources?: Record<string, string>
19+
}) {
20+
const sources = await prepareContractSources(contractName, sourceCode, additionalSources)
1321
const standardJsonInputString = JSON.stringify({
1422
language: "Solidity",
1523
sources,

lib/hooks/use-wallet-deploy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,24 @@ export function useWalletDeploy() {
2424
contractName,
2525
sourceCode,
2626
constructorArgs,
27+
imports,
2728
}: {
2829
contractName: string
2930
sourceCode: string
3031
constructorArgs: Array<string>
32+
imports?: Record<string, string>
3133
}) => {
3234
if (!viemChain || !walletClient || !address || !chainId) {
3335
return
3436
}
3537
const deployLoadingToast = toast.loading("Deploying contract...")
3638

3739
try {
38-
const { abi, bytecode, standardJsonInput, sources } = await compileContract({ contractName, sourceCode })
40+
const { abi, bytecode, standardJsonInput, sources } = await compileContract({
41+
contractName,
42+
sourceCode,
43+
sources: imports,
44+
})
3945

4046
const parsedConstructorArgs = constructorArgs.map((arg) => {
4147
if (arg.startsWith("[") && arg.endsWith("]") && arg.match(/(?<=\[)(?=[^"'])(.*)(?<=[^"'])(?=\])/g)) {

lib/solidity/deploy.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,19 @@ export const deployContract = async ({
3333
contractName,
3434
sourceCode,
3535
constructorArgs,
36+
imports,
3637
}: DeployContractParams): Promise<DeployContractResult> => {
3738
const viemChain = getChainById(Number(chainId))
3839

3940
if (!viemChain) {
4041
throw new Error(`Chain ${chainId} not found`)
4142
}
4243

43-
const { abi, bytecode, standardJsonInput, sources } = await compileContract({ contractName, sourceCode })
44+
const { abi, bytecode, standardJsonInput, sources } = await compileContract({
45+
contractName,
46+
sourceCode,
47+
sources: imports,
48+
})
4449

4550
const walletClient = createWalletClient({
4651
account: DEPLOYER_ACCOUNT,

lib/solidity/utils.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
// Recursive function to resolve imports in a source code. Fetches the source code of the imports one by one and returns the final source code with all imports resolved and urls / aliases replaced with relative paths.
2-
export async function resolveImports(sourceCode: string, sourcePath?: string) {
2+
export async function resolveImports(sourceCode: string, sourcePath?: string, localSources?: Record<string, string>) {
33
const sources: { [fileName: string]: { content: string } } = {}
44
const importRegex = /import\s+(?:{[^}]+}\s+from\s+)?["']([^"']+)["'];/g
55
const matches = Array.from(sourceCode.matchAll(importRegex))
66
let sourceCodeWithImports = sourceCode
77
for (const match of matches) {
88
const importPath = match[1]
9-
const { sources: importedSources, sourceCode: mainSourceCode } = await fetchImport(importPath, sourcePath)
9+
const { sources: importedSources, sourceCode: mainSourceCode } = await fetchImport(
10+
importPath,
11+
sourcePath,
12+
localSources,
13+
)
1014

1115
// Merge the imported sources into the main sources object
1216
Object.assign(sources, importedSources)
@@ -21,12 +25,26 @@ export async function resolveImports(sourceCode: string, sourcePath?: string) {
2125
sources[sourceFileName] = {
2226
content: mainSourceCode,
2327
}
24-
sourceCodeWithImports = sourceCode.replace(match[0], `import "${sourceFileName}";`)
28+
sourceCodeWithImports = sourceCodeWithImports.replace(match[0], `import "${sourceFileName}";`)
2529
}
2630
return { sources, sourceCode: sourceCodeWithImports }
2731
}
2832

29-
async function fetchImport(importPath: string, sourcePath?: string) {
33+
async function fetchImport(importPath: string, sourcePath?: string, localSources?: Record<string, string>) {
34+
// Check if the import exists in provided local sources
35+
if (localSources) {
36+
let localPath = importPath
37+
if (importPath[0] === "." && sourcePath) {
38+
localPath = resolveImportPath(importPath, sourcePath)
39+
}
40+
localPath = localPath.replace(/^\.\//, "")
41+
if (localSources[localPath]) {
42+
const importedSource = localSources[localPath]
43+
const { sources, sourceCode } = await resolveImports(importedSource, localPath, localSources)
44+
return { sources, sourceCode }
45+
}
46+
}
47+
3048
// Determine the URL to fetch
3149
let urlToFetch: string
3250
if (importPath[0] === "." && sourcePath) {
@@ -55,7 +73,7 @@ async function fetchImport(importPath: string, sourcePath?: string) {
5573
const importedSource = await response.text()
5674

5775
// Handle any imports within the fetched source code
58-
const { sources, sourceCode } = await resolveImports(importedSource, urlToFetch)
76+
const { sources, sourceCode } = await resolveImports(importedSource, urlToFetch, localSources)
5977

6078
return { sources, sourceCode }
6179
}
@@ -88,10 +106,14 @@ export const getContractFileName = (contractName: string): string => {
88106
return `${contractName.replace(/[/\\:*?"<>|.\s]+$/g, "_")}.sol`
89107
}
90108

91-
export async function prepareContractSources(contractName: string, sourceCode: string) {
109+
export async function prepareContractSources(
110+
contractName: string,
111+
sourceCode: string,
112+
localSources?: Record<string, string>,
113+
) {
92114
const fileName = getContractFileName(contractName)
93115

94-
const handleImportsResult = await resolveImports(sourceCode)
116+
const handleImportsResult = await resolveImports(sourceCode, fileName, localSources)
95117

96118
const sources = {
97119
[fileName]: {

lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type DeployContractParams = {
4949
contractName: string
5050
sourceCode: string
5151
constructorArgs: Array<string | string[]>
52+
imports?: Record<string, string>
5253
}
5354

5455
export type DeployContractResult = {

0 commit comments

Comments
 (0)