Skip to content

Commit

Permalink
feat: Add more options to deploying contract & CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
pawanpaudel93 committed Mar 21, 2024
1 parent eef2593 commit 3dc0cc5
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 45 deletions.
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@ Usage: ao-deploy [options] <contractPath>
A CLI tool to deploy AO contracts

Arguments:
contractPath Contract main file path to deploy
contractPath Contract main file path to deploy

Options:
-V, --version output the version number
-n, --name [name] Name of contract to deploy (default: "default")
-w, --wallet-path [walletPath] Wallet JWK file path
-h, --help display help for command
-V, --version output the version number
-n, --name [name] Name of the process to spawn (default: "default")
-w, --wallet [wallet] Wallet JWK file path
-s, --scheduler [scheduler] Scheduler to use for Process
-m, --module [module] The module source to use to spin up Process
-c, --cron [interval] Cron interval to use for Process i.e (1-minute, 5-minutes)
-t, --tags [tags...] Additional tags to use when spawning Process
--retry-count [count] Retry count to spawn Process (default: "10")
--retry-delay [delay] Retry delay in seconds (default: "3000")
-h, --help display help for command
```

#### Example
Expand Down Expand Up @@ -81,12 +87,24 @@ import { deployContract } from 'ao-deploy'

async function main() {
try {
const { messageId, processId } = await deployContract({ name: 'demo', walletPath: 'wallet.json', contractPath: 'process.lua' })
const { messageId, processId } = await deployContract(
{
name: 'demo',
wallet: 'wallet.json',
contractPath: 'process.lua',
tags: [{ name: 'Custom', value: 'Tag' }],
retry: {
count: 10,
delay: 3000,
},
},
)
const processUrl = `https://ao_marton.g8way.io/#/process/${processId}`
console.log(`\nDeployed Process: ${processUrl} \nDeployment Message: ${processUrl}/${messageId}`)
const messageUrl = `${processUrl}/${messageId}`
console.log(`\nDeployed Process: ${processUrl} \nDeployment Message: ${messageUrl}`)
}
catch (error: any) {
console.log('\nDeploy failed!\n')
console.log('\nDeployment failed!\n')
console.log(error?.message ?? 'Failed to deploy contract!')
}
}
Expand Down
53 changes: 45 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,37 @@ import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
import chalk from 'chalk'
import { Command } from 'commander'
import { deployContract } from './lib/deploy'
import { type Tag, deployContract } from './lib/deploy'

const PKG_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '../')

export function getVersion() {
function getVersion() {
const packageJsonPath = path.join(PKG_ROOT, 'package.json')
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString())
return packageJson.version || '1.0.0'
}

function parseToInt(value: string, defaultValue: number) {
const parsedValue = Number.parseInt(value)
if (Number.isNaN(parsedValue))
return defaultValue
return parsedValue
}

const program = new Command()
program
.name('ao-deploy')
.description('A CLI tool to deploy AO contracts')
.version(getVersion())
.argument('<contractPath>', 'Contract main file path to deploy')
.option('-n, --name [name]', 'Name of contract to deploy', 'default')
.option('-w, --wallet-path [walletPath]', 'Wallet JWK file path')
.option('-n, --name [name]', 'Name of the process to spawn', 'default')
.option('-w, --wallet [wallet]', 'Wallet JWK file path')
.option('-s, --scheduler [scheduler]', 'Scheduler to use for Process')
.option('-m, --module [module]', 'The module source to use to spin up Process')
.option('-c, --cron [interval]', 'Cron interval to use for Process i.e (1-minute, 5-minutes)')
.option('-t, --tags [tags...]', 'Additional tags to use when spawning Process')
.option('--retry-count [count]', 'Retry count to spawn Process', '10')
.option('--retry-delay [delay]', 'Retry delay in seconds', '3000')

program.parse(process.argv)

Expand All @@ -33,12 +46,36 @@ const contractPath = program.args[0]
async function main() {
try {
console.log(chalk.gray('Deploying...\n'))
const { messageId, processId } = await deployContract({ name: options.name, walletPath: options.walletPath, contractPath })
const processUrl = `https://ao_marton.g8way.io/#/process/${processId}`
console.log(chalk.green(`\nDeployed Process: ${processUrl} \nDeployment Message: ${processUrl}/${messageId}`))
const tags: Tag[] = Array.isArray(options.tags)
? options.tags.reduce<Tag[]>((accumulator, tag) => {
if (tag && tag.includes(':')) {
const [name, value] = tag.split(':')
accumulator.push({ name, value })
}
return accumulator
}, [])
: []
const { messageId, processId } = await deployContract(
{
name: options.name,
wallet: options.wallet,
contractPath,
scheduler: options.scheduler,
module: options.module,
cron: options.cron,
tags,
retry: {
count: parseToInt(options.retryCount, 10),
delay: parseToInt(options.retryDelay, 3000),
},
},
)
const processUrl = chalk.green(`https://ao_marton.g8way.io/#/process/${processId}`)
const messageUrl = chalk.green(`${processUrl}/${messageId}`)
console.log(`\nDeployed Process: ${processUrl} \nDeployment Message: ${messageUrl}`)
}
catch (error: any) {
console.log(chalk.red('\nDeploy failed!\n'))
console.log(chalk.red('\nDeployment failed!\n'))
console.log(chalk.red(error?.message ?? 'Failed to deploy contract!'))
}
}
Expand Down
120 changes: 95 additions & 25 deletions src/lib/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,95 @@ import {
spawn,
} from '@permaweb/aoconnect'
import Ardb from 'ardb'
import { arweave, getWallet, getWalletAddress } from './wallet'
import type { JWKInterface } from 'arweave/node/lib/wallet'
import { arweave, getWallet, getWalletAddress, isArweaveAddress } from './wallet'
import { loadContract } from './load'

const ardb = new ((Ardb as any)?.default ?? Ardb)(arweave)

/**
* Args for deployContract
*/
export interface DeployArgs {
/**
* Process name to spawn
* @default "default"
*/
name?: string
walletPath?: string
/**
* Path to contract main file
*/
contractPath: string
/**
* The module source to use to spin up Process
* @default "Fetches from `https://raw.githubusercontent.com/permaweb/aos/main/package.json`"
*/
module?: string
/**
* Scheduler to use for Process
* @default "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA"
*/
scheduler?: string
/**
* Additional tags to use for spawning Process
*/
tags?: Tag[]
/**
* Cron interval to use for Process i.e (1-minute, 5-minutes)
*/
cron?: string
/**
* Wallet path or JWK itself
*/
wallet?: JWKInterface | string

/**
* Retry options
*/
retry?: {
/**
* Retry count
* @default 10
*/
count?: number
/**
* Retry delay in milliseconds
* @default 3000
*/
delay?: number
}
}

export interface Tag { name: string, value: string }

const ardb = new ((Ardb as any)?.default ?? Ardb)(arweave)

/**
* Retries a given function up to a maximum number of attempts.
* @param fn - The asynchronous function to retry, which should return a Promise.
* @param maxAttempts - The maximum number of attempts to make.
* @param delay - The delay between attempts in milliseconds.
* @return A Promise that resolves with the result of the function or rejects after all attempts fail.
*/
async function retry<T>(
async function retryWithDelay<T>(
fn: () => Promise<T>,
maxAttempts: number,
delay: number = 1000,
): Promise<T> {
let attempts = 0

const attempt = (): Promise<T> => {
return fn().catch((error) => {
const attempt = async (): Promise<T> => {
try {
return await fn()
}
catch (error) {
attempts += 1
if (attempts < maxAttempts) {
console.log(`Attempt ${attempts} failed, retrying...`)
return new Promise<T>(resolve =>
setTimeout(() => resolve(attempt()), delay),
)
return new Promise<T>(resolve => setTimeout(() => resolve(attempt()), delay))
}
else {
throw error
}
})
}
}

return attempt()
Expand All @@ -55,19 +106,21 @@ async function sleep(delay: number = 3000) {
async function getAos() {
const defaultVersion = '1.10.22'
const defaultModule = 'SBNb1qPQ1TDwpD_mboxm2YllmMLXpWw4U8P9Ff8W9vk'
const defaultScheduler = '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA'
try {
const pkg = await (
await fetch(
'https://raw.githubusercontent.com/permaweb/aos/main/package.json',
)
).json() as { version: string, aos: { module: string } }
return {
version: pkg?.version ?? defaultVersion,
module: pkg?.aos?.module ?? defaultModule,
aosVersion: pkg?.version ?? defaultVersion,
aosModule: pkg?.aos?.module ?? defaultModule,
aosScheduler: defaultScheduler,
}
}
catch {
return { version: defaultVersion, module: defaultModule }
return { aosVersion: defaultVersion, aosModule: defaultModule, aosScheduler: defaultScheduler }
}
}

Expand Down Expand Up @@ -95,20 +148,37 @@ async function findProcess(name: string, aosModule: string, owner: string) {
return tx?.id
}

export async function deployContract({ name, walletPath, contractPath }: DeployArgs) {
export async function deployContract({ name, wallet, contractPath, tags, cron, module, scheduler, retry }: DeployArgs) {
// Create a new process
name = name || 'default'
const { version, module } = await getAos()
const wallet = await getWallet(walletPath)
const owner = await getWalletAddress(wallet)
let processId = await findProcess(name, module, owner)
const scheduler = '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA'
tags = Array.isArray(tags) ? tags : []
retry = retry ?? { count: 10, delay: 3000 }

const { aosVersion, aosModule, aosScheduler } = await getAos()
module = isArweaveAddress(module) ? module! : aosModule
scheduler = isArweaveAddress(scheduler) ? scheduler! : aosScheduler

const walletJWK = await getWallet(wallet)
const owner = await getWalletAddress(walletJWK)
const signer = createDataItemSigner(wallet)
const tags = [

let processId = await findProcess(name, module, owner)

tags = [
{ name: 'App-Name', value: 'aos' },
{ name: 'Name', value: name },
{ name: 'aos-Version', value: version },
{ name: 'aos-Version', value: aosVersion },
...tags,
]
if (cron) {
if (/^\d+\-(second|seconds|minute|minutes|hour|hours|day|days|month|months|year|years|block|blocks|Second|Seconds|Minute|Minutes|Hour|Hours|Day|Days|Month|Months|Year|Years|Block|Blocks)$/.test(cron)) {
tags = [...tags, { name: 'Cron-Interval', value: cron }, { name: 'Cron-Tag-Action', value: 'Cron' },
]
}
else {
throw new Error('Invalid cron flag!')
}
}
const data = '1984'

if (!processId) {
Expand All @@ -119,16 +189,16 @@ export async function deployContract({ name, walletPath, contractPath }: DeployA
const contractSrc = loadContract(contractPath)

// Load contract to process
const messageId = await retry(
const messageId = await retryWithDelay(
async () =>
message({
process: processId,
tags: [{ name: 'Action', value: 'Eval' }],
data: contractSrc,
signer,
}),
10,
3000,
retry.count ?? 10,
retry.delay ?? 3000,
)

const { Output } = await result({ process: processId, message: messageId })
Expand Down
23 changes: 19 additions & 4 deletions src/lib/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,27 @@ export const arweave = Arweave.init({
protocol: 'https',
})

export async function getWallet(walletPath?: fs.PathOrFileDescriptor) {
/**
* Check if the passed argument is a valid JSON Web Key (JWK) for Arweave.
* @param obj - The object to check for JWK validity.
* @returns {boolean} True if it's a valid Arweave JWK, otherwise false.
*/
function isJwk(obj: any): boolean {
if (typeof obj !== 'object')
return false
const requiredKeys = ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi']
return requiredKeys.every(key => key in obj)
}

export async function getWallet(walletOrPath?: fs.PathOrFileDescriptor | JWKInterface): Promise<JWKInterface> {
try {
if (!walletPath)
throw new Error('Wallet path not specified')
if (!walletOrPath)
throw new Error('Wallet not specified')

if (isJwk(walletOrPath))
return walletOrPath as JWKInterface

const jwk = fs.readFileSync(walletPath, 'utf8')
const jwk = fs.readFileSync(walletOrPath as string, 'utf8')
return JSON.parse(jwk)
}
catch (e) {
Expand Down

0 comments on commit 3dc0cc5

Please sign in to comment.