parent
413a3b155a
commit
9efa9dd973
@ -0,0 +1,15 @@ |
||||
{ |
||||
"extends": "../../.eslintrc", |
||||
"rules": { |
||||
"dot-notation": "off", |
||||
"no-unused-vars": "off", |
||||
"no-use-before-define": "off" |
||||
}, |
||||
"env": { |
||||
"browser": true, |
||||
"amd": true, |
||||
"node": true, |
||||
"es6": true |
||||
}, |
||||
"ignorePatterns": ["!**/*"] |
||||
} |
@ -0,0 +1 @@ |
||||
src/ |
@ -0,0 +1,7 @@ |
||||
# ghaction |
||||
|
||||
This library was generated with [Nx](https://nx.dev). |
||||
|
||||
## Running unit tests |
||||
|
||||
Run `nx test ghaction` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1,40 @@ |
||||
{ |
||||
"name": "@remix-project/ghaction", |
||||
"version": "0.1.0", |
||||
"description": "Tools to help with github actions for running solidity tests", |
||||
"main": "src/index.js", |
||||
"types": "src/index.d.ts", |
||||
"contributors": [ |
||||
{ |
||||
"name": "David Disu", |
||||
"email": "daviddisu8@gmail.com" |
||||
} |
||||
], |
||||
"dependencies": { |
||||
"ethers": "^5.7.2", |
||||
"ganache": "^7.5.0", |
||||
"chai": "^4.3.7", |
||||
"@remix-project/remix-solidity": "^0.5.5", |
||||
"@ethereum-waffle/chai": "^3.4.4" |
||||
}, |
||||
"devDependencies": { |
||||
"typescript": "^3.7.4" |
||||
}, |
||||
"scripts": { |
||||
"build": "tsc" |
||||
}, |
||||
"publishConfig": { |
||||
"access": "public" |
||||
}, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "git+https://github.com/ethereum/remix-project.git" |
||||
}, |
||||
"author": "Remix Team", |
||||
"license": "MIT", |
||||
"bugs": { |
||||
"url": "https://github.com/ethereum/remix-project/issues" |
||||
}, |
||||
"homepage": "https://github.com/ethereum/remix-project/tree/master/libs/ghaction#readme", |
||||
"typings": "src/index.d.ts" |
||||
} |
@ -0,0 +1,22 @@ |
||||
import { CompilationResult } from '@remix-project/remix-solidity' |
||||
import * as fs from 'fs/promises' |
||||
import * as path from 'path' |
||||
|
||||
declare global { |
||||
const remixContractArtefactsPath: string |
||||
} |
||||
|
||||
export async function getArtefactsByContractName (contractIdentifier: string) { |
||||
const contractArtefacts = await fs.readdir(global.remixContractArtefactsPath) |
||||
let contract |
||||
|
||||
for (const artefactFile of contractArtefacts) { |
||||
const artefact = await fs.readFile(path.join(global.remixContractArtefactsPath, artefactFile), 'utf-8') |
||||
const artefactJSON: CompilationResult = JSON.parse(artefact) |
||||
const contractFullPath = (Object.keys(artefactJSON.contracts!)).find((contractName) => artefactJSON.contracts![contractName] && artefactJSON.contracts![contractName][contractIdentifier]) |
||||
|
||||
contract = contractFullPath ? artefactJSON.contracts![contractFullPath!][contractIdentifier] : undefined |
||||
if (contract) break |
||||
} |
||||
return contract |
||||
} |
@ -0,0 +1,7 @@ |
||||
import * as chai from 'chai' |
||||
import { waffleChai } from '@ethereum-waffle/chai' |
||||
|
||||
chai.use(waffleChai) |
||||
|
||||
export * from 'chai' |
||||
export { chai } |
@ -0,0 +1,9 @@ |
||||
import { ethers } from 'ethers' |
||||
import * as ganache from 'ganache' |
||||
import * as hhEtherMethods from './methods' |
||||
|
||||
global.ganacheProvider = ganache.provider({ logging: { quiet: true } }) |
||||
for(const method in hhEtherMethods) Object.defineProperty(ethers, method, { value: hhEtherMethods[method]}) |
||||
|
||||
export * from 'ethers' |
||||
export { ethers } |
@ -0,0 +1,2 @@ |
||||
export * from './chai' |
||||
export * from './ethers' |
@ -0,0 +1,244 @@ |
||||
import { ethers } from "ethers" |
||||
import { getArtefactsByContractName } from './artefacts-helper' |
||||
import { SignerWithAddress } from './signer' |
||||
declare global { |
||||
const ganacheProvider: any |
||||
} |
||||
|
||||
const isFactoryOptions = (signerOrOptions) => { |
||||
if (!signerOrOptions || signerOrOptions === undefined || signerOrOptions instanceof ethers.Signer) return false |
||||
return true |
||||
} |
||||
|
||||
const isArtifact = (artifact) => { |
||||
const { |
||||
contractName, |
||||
sourceName, |
||||
abi, |
||||
bytecode, |
||||
deployedBytecode, |
||||
linkReferences, |
||||
deployedLinkReferences, |
||||
} = artifact |
||||
|
||||
return ( |
||||
typeof contractName === "string" && |
||||
typeof sourceName === "string" && |
||||
Array.isArray(abi) && |
||||
typeof bytecode === "string" && |
||||
typeof deployedBytecode === "string" && |
||||
linkReferences !== undefined && |
||||
deployedLinkReferences !== undefined |
||||
) |
||||
} |
||||
|
||||
function linkBytecode(artifact, libraries) { |
||||
let bytecode = artifact.bytecode |
||||
|
||||
for (const { sourceName, libraryName, address } of libraries) { |
||||
const linkReferences = artifact.linkReferences[sourceName][libraryName] |
||||
for (const { start, length } of linkReferences) { |
||||
bytecode = |
||||
bytecode.substr(0, 2 + start * 2) + |
||||
address.substr(2) + |
||||
bytecode.substr(2 + (start + length) * 2) |
||||
} |
||||
} |
||||
|
||||
return bytecode |
||||
} |
||||
|
||||
const collectLibrariesAndLink = async (artifact, libraries) => { |
||||
const neededLibraries = [] |
||||
for (const [sourceName, sourceLibraries] of Object.entries(artifact.linkReferences)) { |
||||
for (const libName of Object.keys(sourceLibraries)) { |
||||
neededLibraries.push({ sourceName, libName }) |
||||
} |
||||
} |
||||
|
||||
const linksToApply = new Map() |
||||
for (const [linkedLibraryName, linkedLibraryAddress] of Object.entries(libraries)) { |
||||
if (!ethers.utils.isAddress(linkedLibraryAddress)) { |
||||
throw new Error( |
||||
`You tried to link the contract ${artifact.contractName} with the library ${linkedLibraryName}, but provided this invalid address: ${linkedLibraryAddress}` |
||||
) |
||||
} |
||||
|
||||
const matchingNeededLibraries = neededLibraries.filter((lib) => { |
||||
return ( |
||||
lib.libName === linkedLibraryName || |
||||
`${lib.sourceName}:${lib.libName}` === linkedLibraryName |
||||
) |
||||
}) |
||||
|
||||
if (matchingNeededLibraries.length === 0) { |
||||
let detailedMessage |
||||
if (neededLibraries.length > 0) { |
||||
const libraryFQNames = neededLibraries |
||||
.map((lib) => `${lib.sourceName}:${lib.libName}`) |
||||
.map((x) => `* ${x}`) |
||||
.join("\n") |
||||
detailedMessage = `The libraries needed are:
|
||||
${libraryFQNames}` |
||||
} else { |
||||
detailedMessage = "This contract doesn't need linking any libraries." |
||||
} |
||||
throw new Error( |
||||
`You tried to link the contract ${artifact.contractName} with ${linkedLibraryName}, which is not one of its libraries.
|
||||
${detailedMessage}` |
||||
) |
||||
} |
||||
|
||||
if (matchingNeededLibraries.length > 1) { |
||||
const matchingNeededLibrariesFQNs = matchingNeededLibraries |
||||
.map(({ sourceName, libName }) => `${sourceName}:${libName}`) |
||||
.map((x) => `* ${x}`) |
||||
.join("\n") |
||||
throw new Error( |
||||
`The library name ${linkedLibraryName} is ambiguous for the contract ${artifact.contractName}.
|
||||
It may resolve to one of the following libraries: |
||||
${matchingNeededLibrariesFQNs} |
||||
To fix this, choose one of these fully qualified library names and replace where appropriate.` |
||||
) |
||||
} |
||||
|
||||
const [neededLibrary] = matchingNeededLibraries |
||||
|
||||
const neededLibraryFQN = `${neededLibrary.sourceName}:${neededLibrary.libName}` |
||||
|
||||
// The only way for this library to be already mapped is
|
||||
// for it to be given twice in the libraries user input:
|
||||
// once as a library name and another as a fully qualified library name.
|
||||
if (linksToApply.has(neededLibraryFQN)) { |
||||
throw new Error( |
||||
`The library names ${neededLibrary.libName} and ${neededLibraryFQN} refer to the same library and were given as two separate library links.
|
||||
Remove one of them and review your library links before proceeding.` |
||||
) |
||||
} |
||||
|
||||
linksToApply.set(neededLibraryFQN, { |
||||
sourceName: neededLibrary.sourceName, |
||||
libraryName: neededLibrary.libName, |
||||
address: linkedLibraryAddress, |
||||
}) |
||||
} |
||||
|
||||
if (linksToApply.size < neededLibraries.length) { |
||||
const missingLibraries = neededLibraries |
||||
.map((lib) => `${lib.sourceName}:${lib.libName}`) |
||||
.filter((libFQName) => !linksToApply.has(libFQName)) |
||||
.map((x) => `* ${x}`) |
||||
.join("\n") |
||||
|
||||
throw new Error( |
||||
`The contract ${artifact.contractName} is missing links for the following libraries:
|
||||
${missingLibraries}` |
||||
) |
||||
} |
||||
|
||||
return linkBytecode(artifact, [...linksToApply.values()]) |
||||
} |
||||
|
||||
// Convert output.contracts.<filename>.<contractName> in Artifact object compatible form
|
||||
const resultToArtifact = (result) => { |
||||
const { fullyQualifiedName, artefact } = result |
||||
return { |
||||
contractName: fullyQualifiedName.split(':')[1], |
||||
sourceName: fullyQualifiedName.split(':')[0], |
||||
abi: artefact.abi, |
||||
bytecode: artefact.evm.bytecode.object, |
||||
deployedBytecode: artefact.evm.deployedBytecode.object, |
||||
linkReferences: artefact.evm.bytecode.linkReferences, |
||||
deployedLinkReferences: artefact.evm.deployedBytecode.linkReferences |
||||
} |
||||
} |
||||
|
||||
const getContractFactory = async (contractNameOrABI: ethers.ContractInterface, bytecode?: string, signerOrOptions = null) => { |
||||
if (bytecode && contractNameOrABI) { |
||||
return new ethers.ContractFactory(contractNameOrABI, bytecode, signerOrOptions || (new ethers.providers.Web3Provider(ganacheProvider)).getSigner()) |
||||
} else if (typeof contractNameOrABI === 'string') { |
||||
const contract = await getArtefactsByContractName(contractNameOrABI) |
||||
|
||||
if (contract) { |
||||
return new ethers.ContractFactory(contract.abi, contract.evm.bytecode.object, signerOrOptions || (new ethers.providers.Web3Provider(ganacheProvider)).getSigner()) |
||||
} else { |
||||
throw new Error('Contract artefacts not found') |
||||
} |
||||
} else { |
||||
throw new Error('Invalid contract name or ABI provided') |
||||
} |
||||
} |
||||
|
||||
const getContractAt = async (contractNameOrABI: ethers.ContractInterface, address: string, signer = null) => { |
||||
const provider = new ethers.providers.Web3Provider(ganacheProvider) |
||||
|
||||
if(typeof contractNameOrABI === 'string') { |
||||
try { |
||||
const result = await getArtefactsByContractName(contractNameOrABI) |
||||
|
||||
if (result) { |
||||
return new ethers.Contract(address, result.abi, signer || provider.getSigner()) |
||||
} else { |
||||
throw new Error('Contract artefacts not found') |
||||
} |
||||
} catch(e) { throw e } |
||||
} else { |
||||
return new ethers.Contract(address, contractNameOrABI, signer || provider.getSigner()) |
||||
} |
||||
} |
||||
|
||||
const getSigner = async (address: string) => { |
||||
const provider = new ethers.providers.Web3Provider(ganacheProvider) |
||||
const signer = provider.getSigner(address) |
||||
|
||||
return SignerWithAddress.create(signer) |
||||
} |
||||
|
||||
const getSigners = async () => { |
||||
try { |
||||
const provider = new ethers.providers.Web3Provider(ganacheProvider) |
||||
const accounts = await provider.listAccounts() |
||||
|
||||
return await Promise.all( accounts.map((account) => getSigner(account))) |
||||
} catch(err) { throw err } |
||||
} |
||||
|
||||
const getContractFactoryFromArtifact = async (artifact, signerOrOptions = null) => { |
||||
let libraries = {} |
||||
let signer |
||||
|
||||
if (!isArtifact(artifact)) { |
||||
throw new Error( |
||||
`You are trying to create a contract factory from an artifact, but you have not passed a valid artifact parameter.` |
||||
) |
||||
} |
||||
|
||||
if (isFactoryOptions(signerOrOptions)) { |
||||
signer = signerOrOptions.signer; |
||||
libraries = signerOrOptions.libraries ?? {}; |
||||
} else { |
||||
signer = signerOrOptions; |
||||
} |
||||
|
||||
if (artifact.bytecode === "0x") { |
||||
throw new Error( |
||||
`You are trying to create a contract factory for the contract ${artifact.contractName}, which is abstract and can't be deployed.
|
||||
If you want to call a contract using ${artifact.contractName} as its interface use the "getContractAt" function instead.` |
||||
) |
||||
} |
||||
|
||||
const linkedBytecode = await collectLibrariesAndLink(artifact, libraries) |
||||
return new ethers.ContractFactory(artifact.abi, linkedBytecode || artifact.bytecode, signer || (new ethers.providers.Web3Provider(web3Provider)).getSigner()) |
||||
} |
||||
|
||||
const getContractAtFromArtifact = async (artifact, address, signerOrOptions = null) => { |
||||
if (!isArtifact(artifact)) { |
||||
throw new Error( |
||||
`You are trying to create a contract factory from an artifact, but you have not passed a valid artifact parameter.` |
||||
) |
||||
} |
||||
|
||||
return await getContractAt(artifact.abi, address, signerOrOptions) |
||||
} |
||||
|
||||
export { getContractAtFromArtifact, getContractFactoryFromArtifact, getSigners, getSigner, getContractAt, getContractFactory } |
@ -0,0 +1,42 @@ |
||||
import { ethers } from "ethers" |
||||
|
||||
export class SignerWithAddress extends ethers.Signer { |
||||
static async create(signer) { |
||||
return new SignerWithAddress(await signer.getAddress(), signer) |
||||
} |
||||
|
||||
constructor(address, _signer) { |
||||
super() |
||||
this.address = address |
||||
this._signer = _signer |
||||
this.provider = _signer.provider |
||||
} |
||||
|
||||
async getAddress() { |
||||
return this.address |
||||
} |
||||
|
||||
signMessage(message){ |
||||
return this._signer.signMessage(message) |
||||
} |
||||
|
||||
signTransaction(transaction) { |
||||
return this._signer.signTransaction(transaction) |
||||
} |
||||
|
||||
sendTransaction(transaction) { |
||||
return this._signer.sendTransaction(transaction) |
||||
} |
||||
|
||||
connect(provider) { |
||||
return new SignerWithAddress(this.address, this._signer.connect(provider)) |
||||
} |
||||
|
||||
_signTypedData(...params) { |
||||
return this._signer._signTypedData(...params) |
||||
} |
||||
|
||||
toJSON() { |
||||
return `<SignerWithAddress ${this.address}>` |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
{ |
||||
"extends": "../../tsconfig.base.json", |
||||
"compilerOptions": { |
||||
"types": ["jest", "node"] |
||||
}, |
||||
"include": ["**/*.ts"] |
||||
} |
@ -0,0 +1,15 @@ |
||||
{ |
||||
"extends": "./tsconfig.json", |
||||
"compilerOptions": { |
||||
"module": "commonjs", |
||||
"outDir": "../../dist/out-tsc", |
||||
"declaration": true, |
||||
"rootDir": "./src", |
||||
"types": ["node"] |
||||
}, |
||||
"exclude": [ |
||||
"**/*.spec.ts" |
||||
], |
||||
"include": ["**/*.ts"] |
||||
} |
||||
|
Loading…
Reference in new issue