Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example One Click Deployer #240

Closed
wants to merge 6 commits into from
Closed
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
21 changes: 21 additions & 0 deletions .github/actions/build-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Build Action
runs:
using: composite
steps:
- uses: pnpm/action-setup@v2
with:
version: 8.6.5

- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
registry-url: https://registry.npmjs.org
cache: pnpm

- name: pnpm install node modules
run: pnpm i --frozen-lockfile
shell: bash

- name: pnpm build
run: pnpm nx run dapp-console-api:build
shell: bash
22 changes: 22 additions & 0 deletions .github/workflows/deploy-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Deploy Contracts

jobs:
deploy-erc20:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build Action
uses: ./.github/actions/build-action
- name: Set up foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Build Contract Artifacts
run: pnpm nx run @eth-optimism/contracts-ecosystem:compile
shell: bash
- name: Deploy Simple ERC20
uses: ./apps/dapp-console-api/src/actions
with:
args: Example,exa,18
target: ./packages/contracts-ecosystem/src/SimpleERC20.sol:SimpleERC20
buildDir: ./packages/contracts-ecosystem/out
version: 1.0
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
uses: ./.github/actions/setup
- name: Run Tests
run: |
pnpm nx affected --base=$NX_BASE --head=$NX_HEAD --target=test
pnpm nx affected --base=$NX_BASE --head=$NX_HEAD --target=test
3 changes: 3 additions & 0 deletions apps/dapp-console-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
"vitest": "^1.1.3"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@privy-io/server-auth": "^1.7.2",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.1",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
Expand Down
5 changes: 3 additions & 2 deletions apps/dapp-console-api/src/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { connectToDatabase, runMigrations } from './db'
import { metrics } from './monitoring/metrics'
import { AuthRoute } from './routes/auth'
import { WalletsRoute } from './routes/wallets'
import { DeploymentRoute } from './routes/Deployments'
import { Trpc } from './Trpc'
import { retryWithBackoff } from './utils'

Expand Down Expand Up @@ -125,11 +126,12 @@ export class Service {

const authRoute = new AuthRoute(trpc)
const walletsRoute = new WalletsRoute(trpc)
const deploymentRoute = new DeploymentRoute(trpc)

/**
* The apiServer simply assmbles the routes into a TRPC Server
*/
const apiServer = new ApiV0(trpc, { authRoute, walletsRoute })
const apiServer = new ApiV0(trpc, { authRoute, deploymentRoute, walletsRoute })
apiServer.setLoggingServer(logger)

const adminServer = new AdminApi(trpc, {})
Expand Down Expand Up @@ -302,7 +304,6 @@ export class Service {
}

private readonly routes = async (router: Router) => {
router.use(this.middleware.cors)
// These handlers do nothing. they pass on the request to the next handler.
// They are a hack I added because our trpc middleware does not expose the supported routes
// in the canonical way and we need a way to filter invalid request paths from being logged to
Expand Down
17 changes: 17 additions & 0 deletions apps/dapp-console-api/src/actions/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: 'Deploy Contract'
description: 'Create2 Factory'
inputs:
args:
description: 'Contract constructor arguments comma seperated'
target:
description: 'Path and contract to deploy <path>/<to>/<contract>.sol:<contract name>'
required: true
version:
description: 'Verision of contract that will be deployed'
required: true
outputs:
deployment:
description: 'Deployment data'
runs:
using: 'node20'
main: '../../build/actions/index.js'
14 changes: 14 additions & 0 deletions apps/dapp-console-api/src/actions/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import superjson from 'superjson'

import type { ApiV0 } from '@/api/ApiV0'

export const apiClient = createTRPCProxyClient<ApiV0['handler']>({
links: [
httpBatchLink({
url: 'http://host.docker.internal:7300/api/v0',
headers: {},
}),
],
transformer: superjson,
})
52 changes: 52 additions & 0 deletions apps/dapp-console-api/src/actions/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import fs from 'fs'

import { apiClient } from './client'

export type DeployArgs = {
args?: string
buildDir?: string
target: string
version: string
}

export async function deploy({ args, buildDir, target, version }: DeployArgs) {
const constructorArgs = args?.split(',')

const [contractPath, contractName] = target.split(':')
const contractDir = contractPath.split('/')
const contractFile = contractDir.pop() ?? ''

const buildOutput = fs.readFileSync(
`${buildDir}/${contractFile}/${contractName}.json`,
)
const buildJSON = JSON.parse(buildOutput.toString())

const bytecode = buildJSON['bytecode']['object']
const abi = buildJSON['abi']

console.log(`########## Bytecode: ${contractFile}:${contractName} ##########`)
console.log(bytecode)
console.log('\n')

console.log(`########## ABI: ${contractFile}:${contractName} ##########`)
console.log(abi)
console.log('\n')

let hasError = false

try {
const res = await apiClient.deployments.triggerDeployment.mutate({
bytecode,
abi,
version,
constructorArgs,
})
console.log(`Contract Address: ${res.address}`)
} catch (e) {
console.error('Error deploying contract: ', e.message)
console.log({ errorMessage: e.message })
hasError = true
}

process.exit(hasError ? 1 : 0)
}
25 changes: 25 additions & 0 deletions apps/dapp-console-api/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as core from '@actions/core'

import { deploy } from './deploy'

export async function main(): Promise<void> {
const constructorArgs = core.getInput('args')
const version = core.getInput('verision')
const target = core.getInput('target')
const buildDir = core.getInput('buildDir')

deploy({
args: constructorArgs,
version,
buildDir,
target,
})
}

;(async () => {
try {
await main()
} catch (e) {
console.log(e)
}
})()
3 changes: 3 additions & 0 deletions apps/dapp-console-api/src/api/ApiV0.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AuthRoute } from '@/routes/auth'
import type { WalletsRoute } from '@/routes/wallets'
import type { DeploymentRoute } from '@/routes/Deployments'

import { MajorApiVersion } from '../constants'
import type { Trpc } from '../Trpc'
Expand Down Expand Up @@ -32,6 +33,7 @@ export class ApiV0 extends Api {
...this.commonRoutes,
[this.routes.authRoute.name]: this.routes.authRoute.handler,
[this.routes.walletsRoute.name]: this.routes.walletsRoute.handler,
[this.routes.deploymentRoute.name]: this.routes.deploymentRoute.handler,
})

/**
Expand All @@ -42,6 +44,7 @@ export class ApiV0 extends Api {
protected readonly routes: {
authRoute: AuthRoute
walletsRoute: WalletsRoute
deploymentRoute: DeploymentRoute
},
) {
super(trpc)
Expand Down
153 changes: 153 additions & 0 deletions apps/dapp-console-api/src/routes/Deployments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { Abi, Address } from 'viem'
import {
createPublicClient,
createWalletClient,
encodeDeployData,
http,
isHex,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { anvil } from 'viem/chains'
import { z } from 'zod'

import { Trpc } from '..'
import { Route } from './Route'

const DEPLOY_SALT =
'0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC'

const create2FactoryContractAddress =
'0x5FbDB2315678afecb367f032d93F642f64180aa3'

const create2FactoryAbi = [
{
type: 'function',
name: 'computeAddress',
inputs: [
{
name: 'bytecode',
type: 'bytes',
internalType: 'bytes',
},
{
name: 'salt',
type: 'bytes32',
internalType: 'bytes32',
},
],
outputs: [
{
name: '',
type: 'address',
internalType: 'address',
},
],
stateMutability: 'view',
},
{
type: 'function',
name: 'deploy',
inputs: [
{
name: 'bytecode',
type: 'bytes',
internalType: 'bytes',
},
{
name: 'salt',
type: 'bytes32',
internalType: 'bytes32',
},
],
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'event',
name: 'DeployedContract',
inputs: [
{
name: 'addr',
type: 'address',
indexed: true,
internalType: 'address',
},
],
anonymous: false,
},
] as Abi

const deployer = createWalletClient({
account: privateKeyToAccount(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
), // anvil index: 0
chain: anvil,
transport: http(),
})

const anvilClient = createPublicClient({
chain: anvil,
transport: http(),
})

export class DeploymentRoute extends Route {
public readonly name = 'deployments' as const

public readonly triggerDeployment = 'triggerDeployment' as const
public readonly triggerDeploymentController = this.trpc.procedure
.input(
z.object({
bytecode: this.z.string().refine(isHex, {
message: `Invalid bytecode hex`,
}),
abi: this.z.any(),
version: this.z.string(),
constructorArgs: this.z.any().array().optional(),
}),
)
.mutation(async ({ input }) => {
const { abi, bytecode, constructorArgs } = input

const data = encodeDeployData({
abi,
bytecode,
args: constructorArgs ?? [],
})

const computedAddress = (await anvilClient.readContract({
abi: create2FactoryAbi,
address: create2FactoryContractAddress,
functionName: 'computeAddress',
args: [data, DEPLOY_SALT],
})) as Address

const code = await anvilClient.getBytecode({ address: computedAddress })
if (code) {
throw Trpc.handleStatus(
400,
`Contract with address ${computedAddress} already exists`,
)
}

const deployTxHash = await deployer.writeContract({
abi: create2FactoryAbi,
address: create2FactoryContractAddress,
functionName: 'deploy',
args: [data, DEPLOY_SALT],
})

const receipt = await anvilClient.waitForTransactionReceipt({
hash: deployTxHash,
})

return {
receipt,
deploymentHash: deployTxHash,
address: computedAddress,
}
})

public readonly handler = this.trpc.router({
[this.triggerDeployment]: this.triggerDeploymentController,
})
}
2 changes: 1 addition & 1 deletion apps/dapp-console-api/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'

export default defineConfig({
name: '@eth-optimism/dapp-console-api',
entry: ['src/index.ts', 'src/cmd/run.ts'],
entry: ['src/index.ts', 'src/cmd/run.ts', 'src/actions/index.ts'],
outDir: 'build',
format: ['cjs'],
target: 'node18',
Expand Down
Loading
Loading