Skip to content

Commit

Permalink
Merge pull request #367 from smartcontractkit/gauntlet-deploy-account
Browse files Browse the repository at this point in the history
gauntlet: add DEPLOY_ACCOUNT action
  • Loading branch information
archseer authored Mar 21, 2024
2 parents 2ee88af + c51a44f commit 1cd31e4
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 17 deletions.
2 changes: 2 additions & 0 deletions packages-ts/starknet-gauntlet-multisig/src/wrapper/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export const wrapCommand = <UI, CI>(
c.executionContext = {
provider: c.provider,
wallet: c.wallet,
category: registeredCommand.id,
action: 'multisig',
id,
contractAddress: c.contractAddress,
flags: flags,
Expand Down
44 changes: 37 additions & 7 deletions packages-ts/starknet-gauntlet-oz/src/commands/account/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const makeUserInput = async (flags, _, env): Promise<UserInput> => {
const keypair = ec.starkCurve.utils.randomPrivateKey()
const generatedPK = '0x' + Buffer.from(keypair).toString('hex')
const pubkey = flags.publicKey || env.publicKey || ec.starkCurve.getStarkKey(keypair)
const salt: number = flags.salt ? +flags.salt : undefined
const salt: number = !isNaN(flags.salt) ? +flags.salt : undefined
return {
publicKey: pubkey,
privateKey: (!flags.publicKey || !env.account) && generatedPK,
Expand All @@ -34,10 +34,22 @@ const makeUserInput = async (flags, _, env): Promise<UserInput> => {
}
}

const validateClassHash = async (input) => {
if (isValidAddress(input.classHash) || input.classHash === undefined) {
const validateClassHash = async (input, executionContext) => {
if (isValidAddress(input.classHash)) {
return true
}

if (input.classHash === undefined) {
// declaring the contract will happen automatically as part of our regular deploy action, but
// deploying account contracts for a new account require an already declared account contract,
// which has to be done from a funded account.
// ref: https://book.starknet.io/ch04-03-deploy-hello-account.html#declaring-the-account-contract
if (executionContext.action === 'deploy-account') {
throw new Error('Account contract has to be declared for a DEPLOY_ACCOUNT action')
}
return true
}

throw new Error(`Invalid Class Hash: ${input.classHash}`)
}

Expand All @@ -52,7 +64,8 @@ const beforeExecute: BeforeExecute<UserInput, ContractInput> = (
) => async () => {
deps.logger.info(`About to deploy an OZ 0.x Account Contract with:
public key: ${input.contract[0]}
salt: ${input.user.salt || 'randomly generated'}`)
salt: ${!isNaN(input.user.salt) ? input.user.salt : 'randomly generated'}
action: ${context.action}`)
if (input.user.privateKey) {
await deps.prompt(`The generated private key will be shown next, continue?`)
deps.logger.line()
Expand All @@ -78,12 +91,12 @@ const afterExecute: AfterExecute<UserInput, ContractInput> = (context, input, de
}
}

const commandConfig: ExecuteCommandConfig<UserInput, ContractInput> = {
const deployCommandConfig: ExecuteCommandConfig<UserInput, ContractInput> = {
contractId: CONTRACT_LIST.ACCOUNT,
category: CATEGORIES.ACCOUNT,
action: 'deploy',
ux: {
description: 'Deploys an OpenZeppelin Account contract',
description: 'Deploys an OpenZeppelin Account contract from an existing account',
examples: [
`${CATEGORIES.ACCOUNT}:deploy --network=<NETWORK> --address=<ADDRESS> --classHash=<CLASS_HASH> <CONTRACT_ADDRESS>`,
],
Expand All @@ -98,4 +111,21 @@ const commandConfig: ExecuteCommandConfig<UserInput, ContractInput> = {
},
}

export default makeExecuteCommand(commandConfig)
const deployAccountCommandConfig: ExecuteCommandConfig<UserInput, ContractInput> = Object.assign(
{},
deployCommandConfig,
{
action: 'deploy-account',
ux: {
description: 'Deploys an OpenZeppelin Account contract using DEPLOY_ACCOUNT',
examples: [
`${CATEGORIES.ACCOUNT}:deploy-account --network=<NETWORK> --address=<ADDRESS> --classHash=<CLASS_HASH> <CONTRACT_ADDRESS>`,
],
},
},
)

const Deploy = makeExecuteCommand(deployCommandConfig)
const DeployAccount = makeExecuteCommand(deployAccountCommandConfig)

export { Deploy, DeployAccount }
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import Deploy from './deploy'
import { Deploy, DeployAccount } from './deploy'
import Declare from './declare'
export default [Deploy, Declare]
export default [Deploy, DeployAccount, Declare]
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { makeProvider } from '@chainlink/starknet-gauntlet'
import deployCommand from '../../src/commands/account/deploy'
import { Deploy } from '../../src/commands/account/deploy'
import { registerExecuteCommand, TIMEOUT, LOCAL_URL } from '@chainlink/starknet-gauntlet/test/utils'
import { accountContractLoader } from '../../src/lib/contracts'
import { Contract } from 'starknet'
Expand All @@ -11,7 +11,7 @@ describe('OZ Account Contract', () => {
it(
'Deployment',
async () => {
const command = await registerExecuteCommand(deployCommand).create({}, [])
const command = await registerExecuteCommand(Deploy).create({}, [])

const report = await command.execute()
expect(report.responses[0].tx.status).toEqual('ACCEPTED')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import deployOZCommand from '@chainlink/starknet-gauntlet-oz/src/commands/account/deploy'
import { Deploy as deployOZCommand } from '@chainlink/starknet-gauntlet-oz/src/commands/account/deploy'
import deployTokenCommand from '../../src/commands/token/deploy'
import mintTokensCommand from '../../src/commands/token/mint'
import transferTokensCommand from '../../src/commands/token/transfer'
Expand Down
52 changes: 52 additions & 0 deletions packages-ts/starknet-gauntlet/src/commands/base/executeCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { IStarknetWallet } from '../../wallet'
import { makeCommandId, Validation, Input } from './command'

export interface ExecutionContext {
category: string
action: string
id: string
contractAddress: string
wallet: IStarknetWallet
Expand Down Expand Up @@ -112,6 +114,8 @@ export const makeExecuteCommand = <UI, CI>(config: ExecuteCommandConfig<UI, CI>)
}

c.executionContext = {
category: config.category,
action: config.action,
provider: c.provider,
wallet: c.wallet,
id: makeCommandId(config.category, config.action, config.suffixes),
Expand Down Expand Up @@ -261,6 +265,52 @@ export const makeExecuteCommand = <UI, CI>(config: ExecuteCommandConfig<UI, CI>)
return tx
}

deployAccountContract = async (): Promise<TransactionResponse> => {
deps.logger.info(`Deploying account contract ${config.category}`)
await deps.prompt('Continue?')
deps.logger.loading(`Sending transaction...`)

// classHash has to be provided, we can't declare on new accounts.
const classHash: string = this.input?.user?.['classHash']
const salt = this.input?.user?.['salt']
const contractInput = this.input.contract as any

const newAccountAddress = hash.calculateContractAddressFromHash(
salt,
classHash,
contractInput,
0,
)
deps.logger.info(
`Add funds to pay for deploy fees to the account address: ${newAccountAddress}`,
)
await deps.prompt('Funded?')

const tx: TransactionResponse = await this.provider.deployAccountContract(
classHash,
this.input.contract,
false,
salt,
)

if (tx.hash === undefined) {
deps.logger.error(`No tx hash found: \n${JSON.stringify(tx, null, 2)}`)
return tx
}

deps.logger.loading(`Waiting for tx confirmation at ${tx.hash}...`)
const response = await tx.wait()
if (!response.success) {
deps.logger.error(`Contract was not deployed: ${tx.errorMessage}`)
return tx
}
deps.logger.success(`Contract deployed on ${tx.hash} with address ${tx.address}`)
deps.logger.info(
`If using RDD, change the RDD ID with the new contract address: ${tx.address}`,
)
return tx
}

executeWithSigner = async (): Promise<TransactionResponse> => {
const pubkey = await this.wallet.getPublicKey()
deps.logger.info(`Using wallet: ${pubkey}`)
Expand All @@ -287,6 +337,8 @@ export const makeExecuteCommand = <UI, CI>(config: ExecuteCommandConfig<UI, CI>)
tx = await this.deployContract()
} else if (config.action === 'declare') {
tx = await this.declareContract()
} else if (config.action === 'deploy-account') {
tx = await this.deployAccountContract()
} else {
tx = await this.executeWithSigner()
}
Expand Down
30 changes: 27 additions & 3 deletions packages-ts/starknet-gauntlet/src/provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TransactionResponse } from '../transaction'
import {
RpcProvider as StarknetProvider,
DeclareContractResponse,
InvokeFunctionResponse,
DeployContractResponse,
CompiledContract,
Expand All @@ -27,6 +28,12 @@ interface IProvider<P> {
wait?: boolean,
salt?: number,
) => Promise<TransactionResponse>
deployAccountContract: (
classHash: string,
input: any,
wait?: boolean,
salt?: number,
) => Promise<TransactionResponse>
declareContract: (
contract: CompiledContract,
compiledClassHash?: string,
Expand All @@ -45,7 +52,7 @@ export const makeProvider = (

export const wrapResponse = (
provider: IStarknetProvider,
response: InvokeFunctionResponse | DeployContractResponse,
response: InvokeFunctionResponse | DeployContractResponse | DeclareContractResponse,
address?: string,
): TransactionResponse => {
const txResponse: TransactionResponse = {
Expand Down Expand Up @@ -121,7 +128,7 @@ class Provider implements IStarknetProvider {
const tx = await this.account.declareAndDeploy({
contract,
compiledClassHash,
salt: salt ? '0x' + salt.toString(16) : salt, // convert number to hex or leave undefined
salt: !isNaN(salt) ? '0x' + salt.toString(16) : salt, // convert number to hex or leave undefined
// unique: false,
...(!!input && input.length > 0 && { constructorCalldata: input }),
})
Expand Down Expand Up @@ -155,7 +162,7 @@ class Provider implements IStarknetProvider {
deployContract = async (classHash: string, input: any = [], wait = true, salt = undefined) => {
const tx = await this.account.deployContract({
classHash: classHash,
salt: salt ? '0x' + salt.toString(16) : salt,
salt: !isNaN(salt) ? '0x' + salt.toString(16) : salt,
...(!!input && input.length > 0 && { constructorCalldata: input }),
})
const response = wrapResponse(this, tx)
Expand All @@ -165,6 +172,23 @@ class Provider implements IStarknetProvider {
return response
}

/**
* Deploys an account contract using DEPLOY_ACCOUNT given a class hash
*/

deployAccountContract = async (classHash: string, input: any = [], wait = true, salt = 0) => {
const tx = await this.account.deployAccount({
classHash: classHash,
constructorCalldata: input,
addressSalt: '0x' + salt.toString(16),
})
const response = wrapResponse(this, tx)

if (!wait) return response
await response.wait()
return response
}

signAndSend = async (calls: Call[], wait = false) => {
const tx = await this.account.execute(calls)
const response = wrapResponse(this, tx)
Expand Down
9 changes: 7 additions & 2 deletions packages-ts/starknet-gauntlet/src/transaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { InvokeFunctionResponse, RPC } from 'starknet'
import {
InvokeFunctionResponse,
DeclareContractResponse,
DeployContractResponse,
RPC,
} from 'starknet'

export type TransactionResponse = {
hash: string
address?: string
wait: () => Promise<{ success: boolean }>
tx?: InvokeFunctionResponse
tx?: InvokeFunctionResponse | DeclareContractResponse | DeployContractResponse
code?: RPC.SPEC.TXN_STATUS
status: 'PENDING' | 'ACCEPTED' | 'REJECTED'
errorMessage?: string
Expand Down

0 comments on commit 1cd31e4

Please sign in to comment.