├── .eslintignore ├── .nvmrc ├── src ├── lib │ ├── git │ │ ├── git.test.ts │ │ └── git.ts │ ├── ens.d.ts │ ├── error.js │ ├── it-stream.ts │ ├── ens.ts │ ├── job.ts │ ├── fs.ts │ ├── exchange │ │ ├── graph.ts │ │ └── contracts.ts │ ├── cliux.ts │ ├── wallet.ts │ └── estuary.ts ├── index.ts ├── config.ts ├── scripts │ ├── check-ens.ts │ └── nock-record.ts ├── commands │ ├── account │ │ ├── address.ts │ │ ├── balance.ts │ │ ├── remove.ts │ │ ├── makeitrain.ts │ │ └── add.ts │ ├── job │ │ ├── info.ts │ │ ├── accept.ts │ │ ├── refund.ts │ │ ├── complete.ts │ │ ├── list.ts │ │ └── submit.ts │ ├── file │ │ ├── list.ts │ │ ├── push.ts │ │ └── pull.ts │ └── app │ │ ├── list.ts │ │ └── info.ts ├── hooks │ └── init │ │ ├── load-from-ens.ts │ │ └── header.ts ├── base.ts ├── constants.ts └── abis │ ├── exchange.json │ ├── erc20.json │ └── testusd.json ├── bin ├── dev.cmd ├── run.cmd ├── run └── dev ├── .eslintrc ├── .vscode └── settings.json ├── test ├── tsconfig.json ├── commands │ ├── file │ │ ├── list.test.ts │ │ ├── pull.test.ts │ │ └── push.test.ts │ ├── app.test.ts │ ├── file.test.ts │ └── app │ │ └── list.test.ts ├── helpers │ └── init.js └── hooks │ └── init │ └── header.test.ts ├── .editorconfig ├── .mocharc.json ├── .gitignore ├── .github └── dependabot.yml ├── tsconfig.json ├── .circleci └── config.yml ├── docs ├── content │ ├── tutorial │ │ └── intro.mdx │ └── using the CLI │ │ └── commands.mdx ├── render.js ├── cli.api.json └── helpdata.json ├── LICENSE ├── config.yml ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.14.0 2 | -------------------------------------------------------------------------------- /src/lib/git/git.test.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {run} from '@oclif/core' 2 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /src/lib/ens.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'content-hash'; 2 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ], 6 | "indent": ["error", 2] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown.preview.breaks": true, 3 | "html.completion.attributeDefaultValue": "singlequotes" 4 | } 5 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/core').run() 4 | .then(require('@oclif/core/flush')) 5 | .catch(require('../src/utils/error')) -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import Conf from 'conf' 2 | import constants from './constants' 3 | 4 | const userConfig = new Conf(constants as any) 5 | 6 | export default userConfig 7 | -------------------------------------------------------------------------------- /src/scripts/check-ens.ts: -------------------------------------------------------------------------------- 1 | import getLabDAOCID from '../lib/ens' 2 | 3 | async function run() { 4 | const record = await getLabDAOCID() 5 | console.log(record) 6 | } 7 | 8 | run() 9 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [ 7 | {"path": ".."} 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "test/helpers/init.js", 4 | "ts-node/register" 5 | ], 6 | "watch-extensions": [ 7 | "ts" 8 | ], 9 | "recursive": true, 10 | "reporter": "spec", 11 | "timeout": 60000 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | /yarn.lock 9 | node_modules 10 | oclif.manifest.json 11 | /docs/helpdata.json 12 | docs/.docz 13 | tsconfig.tsbuildinfo 14 | TODO.md 15 | -------------------------------------------------------------------------------- /src/lib/error.js: -------------------------------------------------------------------------------- 1 | const logger = require('@oclif/core/lib/cli-ux/index').logger 2 | 3 | module.exports = (error) => { 4 | const oclifHandler = require('@oclif/errors/handle') 5 | // do any extra work with error 6 | logger.error(error) 7 | return oclifHandler(error) 8 | } 9 | -------------------------------------------------------------------------------- /test/commands/file/list.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('file list', () => { 4 | test 5 | .stdout() 6 | .command(['file list']) 7 | .it('runs', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/helpers/init.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line unicorn/prefer-module 2 | const path = require('path') 3 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') 4 | process.env.NODE_ENV = 'development' 5 | 6 | global.oclif = global.oclif || {} 7 | global.oclif.columns = 80 8 | -------------------------------------------------------------------------------- /test/hooks/init/header.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('hooks', () => { 4 | test 5 | .stdout() 6 | .hook('init', {id: 'mycommand'}) 7 | .do(output => expect(output.stdout).to.contain('example hook running mycommand')) 8 | .it('shows a message') 9 | }) 10 | -------------------------------------------------------------------------------- /src/lib/git/git.ts: -------------------------------------------------------------------------------- 1 | import child_process from 'child_process' 2 | import path from 'path' 3 | 4 | const basedir = path.resolve(__dirname, '../..') 5 | export default async function shorthash(repopath: string) { 6 | return child_process 7 | .execSync('git rev-parse --short HEAD', { cwd: basedir }) 8 | .toString().trim() 9 | } 10 | -------------------------------------------------------------------------------- /test/commands/app.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('app', () => { 4 | test 5 | .stdout() 6 | .command(['app']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['app', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/file.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('file', () => { 4 | test 5 | .stdout() 6 | .command(['file']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['file', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/file/pull.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('file pull', () => { 4 | test 5 | .stdout() 6 | .command(['file pull']) 7 | .it('requires CID', ctx => { 8 | expect(ctx.stdout).to.throw('required') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['file pull example_cid']) 14 | .it('runs with CID', ctx => { 15 | expect(ctx.stdout).to.contain('cid = example_cid') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/app/list.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('app:list', () => { 4 | test 5 | .stdout() 6 | .command(['app:list']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['app:list', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /test/commands/file/push.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@oclif/test' 2 | 3 | describe('file/push', () => { 4 | test 5 | .stdout() 6 | .command(['file/push']) 7 | .it('runs hello', ctx => { 8 | expect(ctx.stdout).to.contain('hello world') 9 | }) 10 | 11 | test 12 | .stdout() 13 | .command(['file/push', '--name', 'jeff']) 14 | .it('runs hello --name jeff', ctx => { 15 | expect(ctx.stdout).to.contain('hello jeff') 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /src/commands/account/address.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | import { loadKeystore } from '../../lib/wallet' 3 | 4 | export default class AccountAddress extends Command { 5 | static description = 'Get the address of your local ETH wallet' 6 | static examples = [ 7 | 'openlab account address', 8 | ] 9 | public async run(): Promise { 10 | const account = loadKeystore() 11 | this.log(`0x${account.address}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | versioning-strategy: increase 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | labels: 9 | - "dependencies" 10 | open-pull-requests-limit: 100 11 | pull-request-branch-name: 12 | separator: "-" 13 | ignore: 14 | - dependency-name: "fs-extra" 15 | - dependency-name: "*" 16 | update-types: ["version-update:semver-major"] 17 | -------------------------------------------------------------------------------- /src/lib/it-stream.ts: -------------------------------------------------------------------------------- 1 | import * as util from 'util' 2 | import * as stream from 'stream' 3 | import * as fs from 'fs' 4 | 5 | const pipeline = util.promisify(stream.pipeline) 6 | 7 | export default async function writeIterableToFile(iterable: Iterable | AsyncIterable, filePath: fs.PathLike) { 8 | const readable = stream.Readable.from( 9 | iterable, {encoding: 'utf8'} 10 | ) 11 | const writable = fs.createWriteStream(filePath) 12 | await pipeline(readable, writable) 13 | } 14 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | const path = require('path') 6 | const project = path.join(__dirname, '..', 'tsconfig.json') 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development' 10 | 11 | require('ts-node').register({project}) 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(require('../src/lib/error')) 18 | -------------------------------------------------------------------------------- /src/lib/ens.ts: -------------------------------------------------------------------------------- 1 | import {ethers} from 'ethers' 2 | const chash = require('content-hash') 3 | 4 | const provider = new ethers.providers.InfuraProvider( 5 | ethers.providers.getNetwork('homestead'), 6 | '38d5a8ea149544eb9bac413ce7c40c8a' 7 | ) 8 | 9 | export default async function getLabDAOCID() { 10 | const resolver = await provider.getResolver('labdao.eth') 11 | if (resolver) { 12 | const cid = await resolver.getContentHash().catch( 13 | e => chash.decode(e.data) 14 | ) 15 | return cid 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/init/load-from-ens.ts: -------------------------------------------------------------------------------- 1 | import {Hook} from '@oclif/core' 2 | import axios from 'axios' 3 | 4 | import getLabDAOCID from '../../lib/ens' 5 | import userConfig from '../../config' 6 | 7 | const hook: Hook<'init'> = async function () { 8 | const cid = getLabDAOCID() 9 | const configURI = `https://${cid}.ipfs.dweb.link` 10 | const res = await axios.get(configURI) 11 | const config = res.data 12 | userConfig.set('latest', config) 13 | this.log(`Verified latest config from labdao.eth`) 14 | } 15 | 16 | export default hook 17 | -------------------------------------------------------------------------------- /src/lib/job.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import axios from 'axios' 3 | 4 | export async function loadJobRequest(path: string) { 5 | // check if path is a local filesystem path 6 | if (fs.existsSync(path)) { 7 | return JSON.parse(fs.readFileSync(path, 'utf-8')) 8 | } 9 | // try to resolve as an IPFS CID 10 | try { 11 | const cid = path.replace('ipfs://', '') 12 | const res = await axios.get(`https://ipfs.infura.io/ipfs/${cid}`) 13 | if (res.data) { 14 | return res.data 15 | } 16 | } catch (e) { 17 | throw new Error('Invalid job request') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "importHelpers": true, 6 | "module": "commonjs", 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "ESNext", 11 | "allowSyntheticDefaultImports": true, 12 | // "noImplicitAny": true, 13 | "esModuleInterop": true, 14 | "resolveJsonModule": true, 15 | }, 16 | "typeRoots": [ 17 | "../../node_modules/@types", 18 | "../../node_modules/*-types" 19 | ], 20 | "exclude": [ 21 | "../../node_modules", 22 | ], 23 | "include": [ 24 | "src/**/*.ts", 25 | "src/**/*.json", 26 | "src/**/*.d.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | release-management: salesforce/npm-release-management@4 5 | 6 | workflows: 7 | version: 2 8 | test-and-release: 9 | jobs: 10 | - release-management/test-package: 11 | matrix: 12 | parameters: 13 | os: 14 | - linux 15 | - windows 16 | node_version: 17 | - latest 18 | - lts 19 | - maintenance 20 | dependabot-automerge: 21 | triggers: 22 | - schedule: 23 | cron: '0 2,5,8,11 * * *' 24 | filters: 25 | branches: 26 | only: 27 | - main 28 | jobs: 29 | - release-management/dependabot-automerge 30 | 31 | -------------------------------------------------------------------------------- /src/base.ts: -------------------------------------------------------------------------------- 1 | import {Command, Flags} from '@oclif/core' 2 | // import logger from './utils/log' 3 | 4 | export default abstract class extends Command { 5 | log(message: string, level: string) { 6 | console.log({ level, message }) 7 | } 8 | 9 | // async init() { 10 | // // do some initialization 11 | // const {flags} = this.parse(this.constructor) 12 | // this.flags = flags 13 | // } 14 | async catch(err: Error) { 15 | // logger.error(err) 16 | // add any custom logic to handle errors from the command 17 | // or simply return the parent class error handling 18 | // return super.catch(err); 19 | } 20 | // async finally(err) { 21 | // // called after run and catch regardless of whether or not the command errored 22 | // return super.finally(_); 23 | // } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/job/info.ts: -------------------------------------------------------------------------------- 1 | import { Command, CliUx, Flags } from '@oclif/core' 2 | import constants from '../../constants' 3 | import axios from 'axios' 4 | import { jobInfo } from '../../lib/exchange/graph' 5 | 6 | const jobStatus = [ 7 | 'open', 8 | 'active', 9 | 'closed', 10 | 'cancelled' 11 | ] 12 | 13 | export default class JobInfo extends Command { 14 | static description = 'Get detail of a specific job on lab-exchange' 15 | static flags = {} 16 | 17 | static args = [ 18 | { 19 | name: 'jobID', 20 | required: true, 21 | description: 'ID of the job to get info for' 22 | } 23 | ] 24 | 25 | public async run(): Promise { 26 | const { 27 | args, flags 28 | } = await this.parse(JobInfo) 29 | try { 30 | const jobdata = await jobInfo(args.jobID) 31 | CliUx.ux.styledJSON(jobdata) 32 | } catch (e) { 33 | this.log('Error querying OpenLab graph:', e) 34 | this.error('Failed to connect to OpenLab graph') 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/content/tutorial/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: openlab-cli tutorial 3 | description: tutorial for the openlab-cli 4 | package: '@labdao/openlab-cli' 5 | route: '/' 6 | --- 7 | 8 | # 📖 `openlab-cli` tutorial 9 | 10 |
11 |

⏬ Install

12 |

⏬ TOC

13 |

⏬ API

14 |
15 | 16 | --- 17 | 18 | ## 👋 hi! welcome to openlab 19 | 20 | ## What is this? 21 | 22 | 23 | 24 | Openlab is ... 25 | 26 | ## When should I use this? 27 | 28 | ## Install 29 | 30 | ```shell 31 | npm install --global @labdao/openlab-cli 32 | ``` 33 | 34 | ## Can't find what you're looking for? 35 | 36 | - 🤔 [What is openlab?]() 37 | - 🧑‍🤝‍🧑 [Community and help]() 38 | - 💻 [Openlab software: https://openlab.tools]() 39 | - 🚀 [Openlab servies: https://openlab.run]() 40 | - 💸 [Get paid to participate with bounties]() 41 | 42 | -------------------------------------------------------------------------------- /src/lib/fs.ts: -------------------------------------------------------------------------------- 1 | import { statSync, Stats } from 'fs' 2 | import { asyncFolderWalker } from 'async-folder-walker' 3 | import path from 'path' 4 | 5 | export async function isDirectory(path: string) { 6 | try { 7 | const stat = statSync(path) 8 | return stat.isDirectory() 9 | } catch (e) { 10 | return false 11 | } 12 | } 13 | 14 | export interface FSItem { 15 | root: string; 16 | filepath: string; 17 | stat: Stats; 18 | relname: string; 19 | basename: string; 20 | } 21 | 22 | export async function walkDirectory(path: string, cb: Function): Promise { 23 | const walker = asyncFolderWalker(path, { 24 | shaper: dir => dir 25 | }) 26 | for await (const entry of walker) { 27 | const item: FSItem = entry 28 | await cb(item) 29 | } 30 | } 31 | 32 | export function localisePath(inputPath: string) { 33 | return inputPath.split(path.sep).join(path.posix.sep) 34 | } 35 | 36 | export function universalisePath(inputPath: string) { 37 | return inputPath.split(path.posix.sep).join(path.sep) 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/file/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, CliUx } from "@oclif/core" 2 | import { estuaryFsTable, getOrCreateCollection } from "../../lib/cliux" 3 | import { EstuaryAPI } from "../../lib/estuary" 4 | import { loadKeystore } from "../../lib/wallet" 5 | 6 | export default class FileList extends Command { 7 | static enableJsonFlag = false 8 | static description = 'List the files and directories stored in IPFS' 9 | 10 | static examples = [ 11 | '<%= config.bin %> <%= command.id %>', 12 | ] 13 | 14 | static flags = { 15 | ...CliUx.ux.table.flags() 16 | } 17 | 18 | static args = [ 19 | {name: 'path', description: 'Remote path to list', default: '/'} 20 | ] 21 | 22 | public async run(): Promise { 23 | const {args, flags} = await this.parse(FileList) 24 | const account = await loadKeystore() 25 | const collection = await getOrCreateCollection(account.address) 26 | const estuary = new EstuaryAPI() 27 | const data = await estuary.listCollectionFs(collection.uuid, args.path) 28 | estuaryFsTable(data, flags) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | const constants = { 2 | openlab: { 3 | baseUrl: 'https://openlab-cli-gateway.labdao.xyz', 4 | }, 5 | estuary: { 6 | clientKeyUrl: 'https://estuary-auth.labdao.xyz', 7 | clientKey: null, 8 | estuaryApiUrl: 'https://api.estuary.tech', 9 | estuaryUploadUrl: 'https://shuttle-5.estuary.tech' 10 | }, 11 | provider: { 12 | maticMumbai: 'https://rpc-mumbai.matic.today', 13 | alchemyMumbai: 'https://polygon-mumbai.g.alchemy.com/v2/RbQoqBm8Vp-taNd4jg7YRDB5t9QSoAAb' 14 | }, 15 | contracts: { 16 | maticMumbai: { 17 | exchange: '0xfee53bffb6b70593478cd027cb2b52776fd8c064', 18 | exchangeFactory: '0x53Eb5C8EF42D7261C0C2c9B8cF637a13B04f860A', 19 | openLabNFT: '0x29bdc464C50F7680259242E5E2F68ab1FC75C964' 20 | } 21 | }, 22 | tokens: { 23 | maticMumbai: { 24 | USD: '0x7fD2493c6ec0400be7247D6A251F00fdccc17375' 25 | } 26 | }, 27 | subgraphs: { 28 | maticMumbai: { 29 | exchange: 'https://api.thegraph.com/subgraphs/name/acashmoney/openlab-exchange' 30 | } 31 | } 32 | } 33 | 34 | export default constants 35 | -------------------------------------------------------------------------------- /src/commands/account/balance.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | import { globalFlags } from '../../lib/cliux' 3 | import { checkErc20Balance, checkMaticBalance, login } from '../../lib/wallet' 4 | 5 | export default class AccountBalance extends Command { 6 | static aliases: string[] = ['wallet:balance'] 7 | static description = 'Get the balance of your local ETH wallet' 8 | static examples = [ 9 | '<%= config.bin %> <%= command.id %>', 10 | ] 11 | 12 | static flags = { 13 | password: globalFlags.password 14 | } 15 | 16 | static args = [{ 17 | name: 'tokenSymbol', 18 | description: 'symbol of the ERC20 token', 19 | default: 'USD' 20 | }] 21 | 22 | public async run(): Promise { 23 | const { 24 | args, flags 25 | } = await this.parse(AccountBalance) 26 | 27 | const account = await login(flags.password) 28 | const erc20Balance = await checkErc20Balance(account.address) 29 | const maticBalance = await checkMaticBalance(account.address) 30 | this.log(`MATIC balance: ${maticBalance}`) 31 | this.log(`${args.tokenSymbol} balance: ${erc20Balance}`) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/account/remove.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command, Flags } from '@oclif/core' 2 | import Listr from 'listr' 3 | import { removeWallet } from '../../lib/wallet' 4 | 5 | export default class AccountRemove extends Command { 6 | static aliases: string[] = ['wallet:remove'] 7 | static description = 'Remove your local ETH wallet' 8 | static examples = [ 9 | '<%= config.bin %> <%= command.id %>', 10 | ] 11 | public async run(): Promise { 12 | 13 | const tasks = new Listr([ 14 | { 15 | title: 'Confirm account removal', 16 | task: async () => true 17 | }, 18 | // TODO: offer to back up the wallet to a specified location 19 | { 20 | title: 'Delete wallet', 21 | task: async (ctx, task) => { 22 | task.title = 'Deleting wallet' 23 | await removeWallet() 24 | return 'Wallet deleted - account successfully removed' 25 | } 26 | } 27 | ], {}) 28 | 29 | const confirm = await CliUx.ux.confirm( 30 | 'Are you sure you want to remove your account? This will delete your local wallet (y/n)' 31 | ) 32 | if (confirm) tasks.run() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rik Smith-Unna and openlab-cli contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/scripts/nock-record.ts: -------------------------------------------------------------------------------- 1 | const nock = require('nock') 2 | import { Command } from '@oclif/core'; 3 | const fs = require('fs') 4 | import AppList from '../commands/app/list'; 5 | // For each openlab-cli command, record the request and response 6 | // to a file in the test/nock/commands directory. 7 | // 8 | // The file name is the command name, with spaces replaced by dashes. 9 | // 10 | // For example, the command `openlab app list` will be 11 | // recorded to `test/nock/commands/app-list.js`. 12 | async function recordCommand(cmdstring: string, command: any) { 13 | // Start recording the requests 14 | nock.recorder.rec({ 15 | dont_print: true, 16 | output_objects: true, 17 | enable_reqheaders_recording: true, 18 | }) 19 | // Run the command 20 | await command.run() 21 | // Stop recording the requests 22 | const requests = nock.recorder.play() 23 | // Write the requests to a file 24 | const fileName = cmdstring.replace(/ /g, '-') 25 | const filePath = `test/nock/commands/${fileName}.js` 26 | const file = fs.createWriteStream(filePath) 27 | file.write(`module.exports = ${JSON.stringify(requests, null, 2)}`) 28 | file.end() 29 | } 30 | 31 | recordCommand('app list', AppList) 32 | -------------------------------------------------------------------------------- /src/hooks/init/header.ts: -------------------------------------------------------------------------------- 1 | // import {Hook} from '@oclif/core' 2 | 3 | // const CFonts = require('cfonts') 4 | // const cfontConfig = { 5 | // font: 'tiny', // define the font face 6 | // align: 'left', // define text alignment 7 | // colors: ['system'], // define all colors 8 | // background: 'transparent', // define the background color, you can also use `backgroundColor` here as key 9 | // letterSpacing: 1, // define letter spacing 10 | // lineHeight: 1, // define the line height 11 | // space: true, // define if the output text should have empty lines on top and on the bottom 12 | // maxLength: '0', // define how many character can be on one line 13 | // gradient: ['green', 'blue'], // define your two gradient colors 14 | // independentGradient: false, // define if you want to recalculate the gradient for each new line 15 | // transitionGradient: true, // define if this is a transition between colors directly 16 | // env: 'node' // define the environment CFonts is being executed in 17 | // } 18 | 19 | // const hook: Hook<'init'> = async function (opts) { 20 | // this.log(CFonts.render('OPENLAB', cfontConfig).string) 21 | // } 22 | 23 | // export default hook 24 | -------------------------------------------------------------------------------- /src/commands/account/makeitrain.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/core' 2 | import { checkErc20Balance, login, makeItRain } from '../../lib/wallet' 3 | import Listr from 'listr' 4 | import { globalFlags } from '../../lib/cliux' 5 | 6 | export default class MakeItRain extends Command { 7 | static description = 'Mint test USD tokens to your local ETH wallet' 8 | 9 | static examples = [ 10 | 'openlab account makeitrain', 11 | ] 12 | 13 | static flags = { 14 | password: globalFlags.password, 15 | } 16 | 17 | public async run(): Promise { 18 | const { 19 | flags 20 | } = await this.parse(MakeItRain) 21 | const account = await login(flags.password) 22 | const list = new Listr([ 23 | { 24 | title: 'Checking wallet', 25 | task: async () => { 26 | return `Wallet confirmed with address ${account.address}` 27 | } 28 | }, 29 | { 30 | title: 'Mint test USD tokens', 31 | task: async (ctx, task) => { 32 | try { 33 | const res = await makeItRain(account.address) 34 | } catch (e) { 35 | task.skip('Minting failed: ' + (e as Error).message) 36 | return false 37 | } 38 | return 'Test tokens minted' 39 | } 40 | }, 41 | { 42 | title: 'Check wallet balance', 43 | task: async (ctx, task) => { 44 | const erc20Balance = checkErc20Balance(account.address) 45 | return `USD balance: ${erc20Balance}` 46 | } 47 | } 48 | 49 | ], {}) 50 | list.run() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/job/accept.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command } from '@oclif/core' 2 | import { acceptJob } from '../../lib/exchange/contracts' 3 | import Listr from 'listr' 4 | import { globalFlags } from '../../lib/cliux'; 5 | import { login } from '../../lib/wallet' 6 | 7 | export default class JobAccept extends Command { 8 | static description = 'Accept a job on lab-exchange' 9 | 10 | static flags = { 11 | password: globalFlags.password 12 | } 13 | 14 | static args = [ 15 | { name: 'jobId', description: 'ID of the job to accept', required: true }, 16 | ] 17 | 18 | static examples = [ 19 | 'openlab job accept ', 20 | ] 21 | 22 | public async run(): Promise { 23 | const { 24 | args, flags 25 | } = await this.parse(JobAccept) 26 | 27 | const account = await login(flags.password) 28 | 29 | const tasks = new Listr([ 30 | { 31 | title: 'Confirm job acceptance', 32 | task: async () => true 33 | }, 34 | { 35 | title: 'Accept job', 36 | task: async (ctx, task) => { 37 | task.title = 'Accepting job - waiting for contract response' 38 | ctx.tx = await acceptJob(account, args.jobId) 39 | return `Job accepted. Transaction hash: ${ctx.tx}` 40 | } 41 | }, 42 | { 43 | title: 'Get transaction details', 44 | task: async ctx => `https://mumbai.polygonscan.com/tx/${ctx.tx}` 45 | } 46 | ], {}) 47 | const confirm = await CliUx.ux.confirm( 48 | 'Are you sure you want to accept this job?' 49 | ) 50 | if (confirm) tasks.run() 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/commands/job/refund.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command } from '@oclif/core' 2 | import { refundJob } from '../../lib/exchange/contracts' 3 | import Listr from 'listr' 4 | import { login } from '../../lib/wallet' 5 | import { globalFlags } from '../../lib/cliux' 6 | 7 | export default class JobRefund extends Command { 8 | static description = 'Cancel an accepted job on lab-exchange and return funds' 9 | 10 | static flags = { 11 | password: globalFlags.password 12 | } 13 | 14 | static args = [ 15 | { name: 'jobId', description: 'ID of the job to cancel', required: true }, 16 | ] 17 | static examples = [ 18 | 'openlab contract refund ', 19 | ] 20 | 21 | public async run(): Promise { 22 | const { 23 | args, flags 24 | } = await this.parse(JobRefund) 25 | 26 | const account = await login(flags.password as string) 27 | 28 | const tasks = new Listr([ 29 | { 30 | title: 'Confirm job refund', 31 | task: async () => true 32 | }, 33 | { 34 | title: 'Refund job', 35 | task: async (ctx, task) => { 36 | task.title = 'Refunding job - waiting for contract response' 37 | ctx.tx = await refundJob(account, args.jobId) 38 | return `Job refunded. Transaction hash: ${ctx.tx}` 39 | } 40 | }, 41 | { 42 | title: 'Get transaction details', 43 | task: async ctx => `https://mumbai.polygonscan.com/tx/${ctx.tx}` 44 | } 45 | ], {}) 46 | 47 | const confirm = await CliUx.ux.confirm( 48 | 'Are you sure you want to refund this job?' 49 | ) 50 | if (confirm) tasks.run() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/commands/app/list.ts: -------------------------------------------------------------------------------- 1 | import { OpenLabApi, Configuration } from "@labdao/openlab-applayer-client" 2 | import { Command, CliUx } from "@oclif/core" 3 | import constants from '../../constants' 4 | 5 | export default class AppList extends Command { 6 | static enableJsonFlag = false 7 | static description = 'List the applications available on lab-exchange' 8 | 9 | static examples = [ 10 | 'openlab app list', 11 | ] 12 | 13 | static flags = { 14 | ...CliUx.ux.table.flags() 15 | } 16 | 17 | static args = [ 18 | {name: 'provider', description: 'Provider name or URL'} 19 | ] 20 | 21 | public async run(): Promise { 22 | const {args, flags} = await this.parse(AppList) 23 | const path = args.path || '/' 24 | const api = new OpenLabApi(new Configuration({ 25 | basePath: constants.openlab.baseUrl 26 | })) 27 | 28 | let apps 29 | if (flags.json) { 30 | apps = await api.apps() 31 | return console.log(JSON.stringify(apps.data, null, 2)) 32 | } else { 33 | CliUx.ux.action.start(`📋 Fetching app list`) 34 | apps = await api.apps() 35 | CliUx.ux.action.stop() 36 | console.log('🖥️ Available apps:\n') 37 | } 38 | 39 | CliUx.ux.table( 40 | apps.data as any[], 41 | { 42 | name: { 43 | get: row => row.appname 44 | }, 45 | description: { 46 | minWidth: 40 47 | }, 48 | version: { 49 | get: row => row.version, 50 | }, 51 | endpoints: { 52 | get: row => row.endpoints.join('\n') 53 | }, 54 | }, 55 | { 56 | // printLine: this.log, // current oclif.CliUx bug: https://github.com/oclif/core/issues/377 57 | ...flags 58 | } 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commands/file/push.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command } from '@oclif/core' 2 | import { EstuaryAPI } from '../../lib/estuary' 3 | import { login } from '../../lib/wallet' 4 | import { globalFlags } from '../../lib/cliux' 5 | import { getOrCreateCollection } from '../../lib/cliux' 6 | import { isDirectory } from '../../lib/fs' 7 | 8 | export default class FilePush extends Command { 9 | static description = 'Push a file from your local filesystem to IPFS' 10 | 11 | static examples = [ 12 | '<%= config.bin %> <%= command.id %>', 13 | ] 14 | 15 | static flags = { 16 | password: globalFlags.password 17 | } 18 | 19 | static args = [ 20 | { 21 | name: 'path', 22 | description: 'Path of local file or directory to push', 23 | required: true 24 | }, 25 | { 26 | name: 'remotepath', 27 | description: 'Remote path where file or directory should be stored', 28 | } 29 | ] 30 | 31 | public async run(): Promise { 32 | const { args, flags } = await this.parse(FilePush) 33 | CliUx.ux.info('Uploading to IPFS') 34 | CliUx.ux.info('To upload to your userspace, you need to authenticate your wallet') 35 | const account = await login(flags.password) 36 | const collection = await getOrCreateCollection(account.address) 37 | const estuary = new EstuaryAPI() 38 | const pathIsDir = await isDirectory(args.path) 39 | let res 40 | if (pathIsDir) { 41 | CliUx.ux.info(`Uploading directory: ${args.path}`) 42 | res = await estuary.pushDirectory( 43 | collection.uuid, args.path, args.remotepath 44 | ) 45 | } else { 46 | res = await estuary.pushFile( 47 | collection.uuid, args.path, args.remotepath 48 | ) 49 | } 50 | console.log(res) 51 | CliUx.ux.styledJSON(res) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/file/pull.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CliUx, 3 | Command, 4 | Flags 5 | } from '@oclif/core' 6 | import download from 'download' 7 | import { 8 | createWriteStream 9 | } from 'fs' 10 | 11 | export default class FilePull extends Command { 12 | static description = 'Pull a remote file from IPFS to your local file system' 13 | 14 | static examples = [ 15 | '<%= config.bin %> <%= command.id %> bafkreictm5biak56glcshkeungckjwf4tf33wxea566dozdyvhrrebnetu -o gp47_tail.fasta', 16 | ] 17 | 18 | static flags = { 19 | outpath: Flags.string({ 20 | char: 'o', 21 | description: 'Path where the pulled file or directory should be stored', 22 | default: '.' 23 | }) 24 | } 25 | 26 | static args = [{ 27 | name: 'CID', 28 | description: 'IPFS content identifier of the file or directory to pull', 29 | required: true 30 | }] 31 | 32 | public async run(): Promise { 33 | const { 34 | args, 35 | flags 36 | } = await this.parse(FilePull) 37 | 38 | flags.outpath = flags.outpath === '[CID]' ? args.CID : flags.outpath 39 | 40 | this.log(`Running pull command with cid = ${args.CID} and outpath = ${flags.outpath}`) 41 | this.log('Downloading file...') 42 | 43 | const dlstream = download( 44 | 'https://dweb.link/ipfs/' + args.CID 45 | ) 46 | 47 | let bar = CliUx.ux.progress({ 48 | 49 | }) 50 | bar.start(100, 0) 51 | 52 | dlstream.on('downloadProgress', p => { 53 | if (p.percent === 1) { 54 | bar.on('redraw-post', () => { 55 | this.log('\nDownload complete') 56 | process.exit(1) 57 | }) 58 | } 59 | bar.update(p.percent * 100) 60 | }) 61 | 62 | dlstream.pipe( 63 | createWriteStream( 64 | flags.outpath 65 | ) 66 | ) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/job/complete.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command } from '@oclif/core' 2 | import { completeContract } from '../../lib/exchange/contracts' 3 | import Listr from 'listr' 4 | import { login } from '../../lib/wallet' 5 | import { globalFlags } from '../../lib/cliux' 6 | 7 | export default class JobComplete extends Command { 8 | static description = 'Complete a job on lab-exchange' 9 | 10 | static flags = { 11 | password: globalFlags.password 12 | } 13 | 14 | static args = [ 15 | { 16 | name: 'jobId', 17 | description: 'ID of the job to complete', 18 | required: true 19 | }, 20 | { 21 | name: 'tokenURI', 22 | description: 'URI of the token to swap', 23 | required: true 24 | } 25 | ] 26 | 27 | static examples = [ 28 | 'openlab job complete ', 29 | ] 30 | 31 | public async run(): Promise { 32 | const { 33 | args, flags 34 | } = await this.parse(JobComplete) 35 | 36 | const account = await login(flags.password) 37 | 38 | const tasks = new Listr([ 39 | { 40 | title: 'Confirm job completion', 41 | task: async () => true 42 | }, 43 | { 44 | title: 'Complete job', 45 | task: async (ctx, task) => { 46 | task.title = 'Completing job - waiting for contract response' 47 | ctx.tx = await completeContract(account, args.jobId, args.tokenURI) 48 | return `Job completed. Transaction hash: ${ctx.tx}` 49 | } 50 | }, 51 | { 52 | title: 'Get transaction details', 53 | task: async ctx => `https://mumbai.polygonscan.com/tx/${ctx.tx}` 54 | } 55 | ], {}) 56 | 57 | const confirm = await CliUx.ux.confirm( 58 | 'Are you sure you want to complete this job?' 59 | ) 60 | if (confirm) tasks.run() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/job/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, CliUx, Flags } from '@oclif/core' 2 | import { jobList, JOB_STATUS } from '../../lib/exchange/graph' 3 | 4 | export default class JobList extends Command { 5 | static description = 'List jobs on lab-exchange' 6 | static flags = { 7 | latest: Flags.string({ 8 | description: 'Number of latest jobs to list', 9 | default: '10', 10 | char: 'l', 11 | exclusive: ['all'] 12 | }), 13 | status: Flags.string({ 14 | description: 'Only list jobs with the given status', 15 | char: 's', 16 | options: JOB_STATUS, 17 | required: false, 18 | default: 'open', 19 | exclusive: ['all'] 20 | }), 21 | all: Flags.boolean({ 22 | description: 'List all jobs', 23 | char: 'a', 24 | exclusive: ['latest'] 25 | }), 26 | ...CliUx.ux.table.flags() 27 | } 28 | 29 | static args = [] 30 | 31 | public async run(): Promise { 32 | const { 33 | args, flags 34 | } = await this.parse(JobList) 35 | try { 36 | const jobs = await jobList(flags.latest, flags.status) 37 | this.logJobs(jobs, flags) 38 | } catch (e) { 39 | this.log('Error querying OpenLab graph:', e) 40 | this.error('Failed to connect to OpenLab graph') 41 | } 42 | } 43 | 44 | async logJobs(jobs: any[], flags: any) { 45 | const columns = Object.fromEntries( 46 | new Map(allUniqueKeys(jobs).map(k => [k, {}])) 47 | ) 48 | CliUx.ux.table( 49 | jobs, 50 | columns, 51 | { 52 | maxWidth: 20, 53 | printLine: this.log.bind(this), 54 | ...flags 55 | } 56 | ) 57 | } 58 | } 59 | 60 | function allUniqueKeys(objects: any[]): string[] { 61 | const keys = new Set() 62 | objects.forEach(row => { 63 | Object.keys(row).forEach(key => keys.add(key)) 64 | }) 65 | return Array.from(keys) 66 | } 67 | -------------------------------------------------------------------------------- /docs/render.js: -------------------------------------------------------------------------------- 1 | const helpdata = require('./helpdata.json') 2 | const { exec } = require("child_process") 3 | const fs = require('fs') 4 | 5 | const transformCommand = c => { 6 | c.globalFlags = c._globalFlags 7 | delete c._globalFlags 8 | return c 9 | } 10 | 11 | const recordExample = async (e, c) => { 12 | console.log('RECORDING EXAMPLE') 13 | const cmd = e 14 | .replace(/<%= config.bin %>/, './bin/dev') 15 | .replace(/<%= command.id %>/, c.id.split(':').join(' ')) 16 | console.log('EXECUTING: ' + cmd) 17 | process.chdir(__dirname + '/..') 18 | return new Promise((resolve, _reject) => { 19 | exec(cmd, (error, stdout, stderr) => { 20 | console.log('OUTPUT:' + stdout + stderr) 21 | resolve({ input: e, output: stdout }) 22 | }) 23 | }) 24 | } 25 | 26 | const recordExamples = async c => { 27 | c.examples = c.examples || [] 28 | const examples = await Promise.all(c.examples.map(e => recordExample(e, c))) 29 | return Object.assign({}, c, { examples }) 30 | } 31 | 32 | const renderCommand = c => { 33 | const md = ` 34 | ## \`openlab ${c.id.split(':').join(' ')}\` 35 | 36 | ${c.description} 37 | 38 | ### args 39 | 40 | ${c.args.map(a => `- \`${a.name}\`: ${a.description}`)} 41 | 42 | ### flags 43 | 44 | ${Object.values(c.flags).map(f => `- \`--${f.name}\`${f.char ? ' / \`-' + f.char + '\`' : ''}: ${f.description}`).join('\n')} 45 | ` 46 | return md 47 | } 48 | 49 | const renderApp = a => a.commands 50 | .map(renderCommand) 51 | .map(c => c.replace(/<%= config.bin %>/, 'openlab')) 52 | .join('\n') 53 | 54 | async function run() { 55 | const output = Object.assign({}, helpdata) 56 | output.commands = await Promise.all( 57 | helpdata.commands 58 | .map(transformCommand) 59 | .map(recordExamples) 60 | ) 61 | 62 | // console.log(renderApp(output)) 63 | 64 | fs.writeFileSync(__dirname + '/cli.api.json', JSON.stringify(helpdata)) 65 | } 66 | 67 | run() -------------------------------------------------------------------------------- /src/commands/app/info.ts: -------------------------------------------------------------------------------- 1 | import { OpenLabApi, Configuration } from "@labdao/openlab-applayer-client" 2 | import { Command, CliUx } from "@oclif/core" 3 | import constants from '../../constants' 4 | 5 | export default class AppInfo extends Command { 6 | static enableJsonFlag = false 7 | static description = 'Get the details of an application on lab-exchange' 8 | 9 | static examples = [ 10 | 'openlab app revcomp', 11 | ] 12 | 13 | static flags = { 14 | ...CliUx.ux.table.flags() 15 | } 16 | 17 | static args = [ 18 | {name: 'appname', description: 'Application name'} 19 | ] 20 | 21 | public async run(): Promise { 22 | const {args, flags} = await this.parse(AppInfo) 23 | CliUx.ux.action.start(`Fetching app details for '${args.appname}'`) 24 | const api2 = new OpenLabApi(new Configuration({ 25 | basePath: constants.openlab.baseUrl 26 | })) 27 | const app = await api2.getApp(args.appname) 28 | CliUx.ux.action.stop() 29 | const a = app.data 30 | CliUx.ux.styledHeader(`${a.appname} v${a.version} from LabDAO Openlab`) 31 | this.log('App name:', a.appname) 32 | this.log('App description:', a.description) 33 | this.log('Available from:', 'LabDAO Openlab', constants.openlab.baseUrl) 34 | const eps = a.endpoints?.map( 35 | ep => { 36 | const url = new URL( 37 | ['v1/apps/', ep].join('/').replace('//', '/'), 38 | constants.openlab.baseUrl 39 | ) 40 | const docsurl = new URL( 41 | ['docs#', ep].join('/').replace('//', '/'), 42 | constants.openlab.baseUrl 43 | ) 44 | return { 45 | endpoint: ep, 46 | url: url.href, 47 | docs: docsurl.href 48 | } 49 | } 50 | ) 51 | CliUx.ux.table( 52 | eps as any[], 53 | { 54 | endpoint: {}, 55 | url: { 56 | minWidth: 40 57 | }, 58 | docs: {} 59 | }, 60 | { 61 | // printLine: this.log, // current oclif.CliUx bug: https://github.com/oclif/core/issues/377 62 | ...flags 63 | } 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/lib/exchange/graph.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import userConfig from "../../config" 3 | 4 | const API_URL: string = userConfig.get( 5 | 'subgraphs.maticMumbai.exchange' 6 | ) as string 7 | export const JOB_STATUS = [ 8 | 'open', 9 | 'active', 10 | 'closed', 11 | 'cancelled' 12 | ] 13 | 14 | interface Job { 15 | id: string 16 | client: string 17 | provider: string 18 | jobCost: number 19 | jobURI: string 20 | openlabNFTURI: string 21 | payableToken: string 22 | status: number | string 23 | request?: any 24 | } 25 | 26 | export async function jobList(latest?: string, status?: string): Promise { 27 | const query = jobListQuery(latest, status) 28 | const result = await axios.post(API_URL, { query }) 29 | if (!result || !result.data || !result.data.data || !result.data.data.jobs) { 30 | console.log(result.data.errors) 31 | throw new Error('No jobs found') 32 | } 33 | const jobs: Job[] = result.data.data.jobs 34 | return jobs.map(labelStatus) 35 | } 36 | 37 | function jobListQuery(latest?: string, status?: string): string { 38 | const latestClause = latest ? 39 | ` last: ${latest}` : '' 40 | const strStatus = JOB_STATUS.indexOf(status as string) 41 | const whereClause = status ? 42 | `where: { jobURI_not: "dummy", status: ${strStatus} }` : '' 43 | const query = [latestClause, whereClause].filter(Boolean).join(' ') 44 | const graphqlQuery = ` 45 | query { 46 | jobs(${query || 'where: { jobURI_not: "dummy" }'}) { 47 | id 48 | client 49 | provider 50 | jobCost 51 | jobURI 52 | openlabNFTURI 53 | payableToken 54 | status 55 | } 56 | } 57 | ` 58 | return graphqlQuery 59 | } 60 | 61 | export async function jobInfo(jobID: string): Promise { 62 | const query = jobInfoQuery(jobID) 63 | const result = await axios.post(API_URL, { query }) 64 | if (!result || !result.data || !result.data.data || !result.data.data.job) { 65 | console.log(result.data.errors) 66 | throw new Error('No job found') 67 | } 68 | const job: Job = result.data.data.job 69 | const cid = job.jobURI.replace('ipfs://', '') 70 | const res = await axios.get(`https://ipfs.infura.io/ipfs/${cid}`) 71 | job.request = res.data 72 | return labelStatus(job) 73 | } 74 | 75 | function jobInfoQuery(jobId: string): string { 76 | return ` 77 | query { 78 | job(id: "${jobId}") { 79 | id 80 | client 81 | provider 82 | jobCost 83 | jobURI 84 | openlabNFTURI 85 | payableToken 86 | status 87 | } 88 | } 89 | ` 90 | } 91 | 92 | function labelStatus(job: Job): Job { 93 | job.status = JOB_STATUS[job.status as number] 94 | return job 95 | } 96 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | # Specify a command to be executed 2 | # like `/bin/bash -l`, `ls`, or any other commands 3 | # the default is bash for Linux 4 | # or powershell.exe for Windows 5 | command: null 6 | 7 | # Specify the current working directory path 8 | # the default is the current working directory path 9 | cwd: null 10 | 11 | # Export additional ENV variables 12 | env: 13 | recording: true 14 | 15 | # Explicitly set the number of columns 16 | # or use `auto` to take the current 17 | # number of columns of your shell 18 | cols: auto 19 | 20 | # Explicitly set the number of rows 21 | # or use `auto` to take the current 22 | # number of rows of your shell 23 | rows: auto 24 | 25 | # Amount of times to repeat GIF 26 | # If value is -1, play once 27 | # If value is 0, loop indefinitely 28 | # If value is a positive number, loop n times 29 | repeat: 0 30 | 31 | # Quality 32 | # 1 - 100 33 | quality: 100 34 | 35 | # Delay between frames in ms 36 | # If the value is `auto` use the actual recording delays 37 | frameDelay: auto 38 | 39 | # Maximum delay between frames in ms 40 | # Ignored if the `frameDelay` isn't set to `auto` 41 | # Set to `auto` to prevent limiting the max idle time 42 | maxIdleTime: 2000 43 | 44 | # The surrounding frame box 45 | # The `type` can be null, window, floating, or solid` 46 | # To hide the title use the value null 47 | # Don't forget to add a backgroundColor style with a null as type 48 | frameBox: 49 | type: floating 50 | title: Terminalizer 51 | style: 52 | border: 0px black solid 53 | # boxShadow: none 54 | # margin: 0px 55 | 56 | # Add a watermark image to the rendered gif 57 | # You need to specify an absolute path for 58 | # the image on your machine or a URL, and you can also 59 | # add your own CSS styles 60 | watermark: 61 | imagePath: null 62 | style: 63 | position: absolute 64 | right: 15px 65 | bottom: 15px 66 | width: 100px 67 | opacity: 0.9 68 | 69 | # Cursor style can be one of 70 | # `block`, `underline`, or `bar` 71 | cursorStyle: block 72 | 73 | # Font family 74 | # You can use any font that is installed on your machine 75 | # in CSS-like syntax 76 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 77 | 78 | # The size of the font 79 | fontSize: 12 80 | 81 | # The height of lines 82 | lineHeight: 1 83 | 84 | # The spacing between letters 85 | letterSpacing: 0 86 | 87 | # Theme 88 | theme: 89 | background: "transparent" 90 | foreground: "#afafaf" 91 | cursor: "#c7c7c7" 92 | black: "#232628" 93 | red: "#fc4384" 94 | green: "#b3e33b" 95 | yellow: "#ffa727" 96 | blue: "#75dff2" 97 | magenta: "#ae89fe" 98 | cyan: "#708387" 99 | white: "#d5d5d0" 100 | brightBlack: "#626566" 101 | brightRed: "#ff7fac" 102 | brightGreen: "#c8ed71" 103 | brightYellow: "#ebdf86" 104 | brightBlue: "#75dff2" 105 | brightMagenta: "#ae89fe" 106 | brightCyan: "#b1c6ca" 107 | brightWhite: "#f9f9f4" 108 | -------------------------------------------------------------------------------- /docs/content/using the CLI/commands.mdx: -------------------------------------------------------------------------------- 1 | 2 | ## `openlab app` 3 | 4 | get application details 5 | 6 | ### args 7 | 8 | - `appname`: name of the application 9 | 10 | ### flags 11 | 12 | - `--json`: Format output as json. 13 | - `--columns`: only show provided columns (comma-separated) 14 | - `--sort`: property to sort by (prepend '-' for descending) 15 | - `--filter`: filter property by partial string matching, ex: name=foo 16 | - `--csv`: output is csv format [alias: --output=csv] 17 | - `--output`: output in a more machine friendly format 18 | - `--extended` / `-x`: show extra columns 19 | - `--no-truncate`: do not truncate output to fit screen 20 | - `--no-header`: hide table header from output 21 | 22 | 23 | ## `openlab app list` 24 | 25 | list applications 26 | 27 | ### args 28 | 29 | - `provider`: provider name or URL 30 | 31 | ### flags 32 | 33 | - `--json`: Format output as json. 34 | - `--columns`: only show provided columns (comma-separated) 35 | - `--sort`: property to sort by (prepend '-' for descending) 36 | - `--filter`: filter property by partial string matching, ex: name=foo 37 | - `--csv`: output is csv format [alias: --output=csv] 38 | - `--output`: output in a more machine friendly format 39 | - `--extended` / `-x`: show extra columns 40 | - `--no-truncate`: do not truncate output to fit screen 41 | - `--no-header`: hide table header from output 42 | 43 | 44 | ## `openlab file list` 45 | 46 | list files 47 | 48 | ### args 49 | 50 | - `path`: remote path to list 51 | 52 | ### flags 53 | 54 | - `--json`: Format output as json. 55 | - `--columns`: only show provided columns (comma-separated) 56 | - `--sort`: property to sort by (prepend '-' for descending) 57 | - `--filter`: filter property by partial string matching, ex: name=foo 58 | - `--csv`: output is csv format [alias: --output=csv] 59 | - `--output`: output in a more machine friendly format 60 | - `--extended` / `-x`: show extra columns 61 | - `--no-truncate`: do not truncate output to fit screen 62 | - `--no-header`: hide table header from output 63 | 64 | 65 | ## `openlab file pull` 66 | 67 | pull a remote file from IPFS to your local file system 68 | 69 | ### args 70 | 71 | - `CID`: the IPFS content identifier of the file or directory to pull 72 | 73 | ### flags 74 | 75 | - `--json`: Format output as json. 76 | - `--outpath` / `-o`: the path where the pulled file or directory should be stored 77 | 78 | 79 | ## `openlab file push` 80 | 81 | push a local file from your storage system to IPFS 82 | 83 | ### args 84 | 85 | - `path`: path of file or directory to push 86 | 87 | ### flags 88 | 89 | - `--json`: Format output as json. 90 | 91 | 92 | ## `openlab help` 93 | 94 | Display help for openlab. 95 | 96 | ### args 97 | 98 | - `command`: Command to show help for. 99 | 100 | ### flags 101 | 102 | - `--nested-commands` / `-n`: Include all nested commands in the output. 103 | 104 | 105 | ## `openlab helpdata` 106 | 107 | Emit help as structured data. 108 | 109 | ### args 110 | 111 | - `command`: Command to show help for. 112 | 113 | ### flags 114 | 115 | - `--nested-commands` / `-n`: Include all nested commands in the output. 116 | 117 | -------------------------------------------------------------------------------- /src/commands/account/add.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command } from '@oclif/core' 2 | import Listr from 'listr' 3 | import { checkMaticBalance, createWallet, drinkFromFaucet, importWallet } from '../../lib/wallet' 4 | 5 | 6 | let string = 'herp derp'; 7 | 8 | export default class AccountAdd extends Command { 9 | // TODO: allow multiple wallets 10 | static aliases: string[] = ['wallet:add'] 11 | static description = 'Add an account by creating or importing an ETH wallet' 12 | static examples = [ 13 | '<%= config.bin %> <%= command.id %>', 14 | ] 15 | 16 | public async run(): Promise { 17 | const newWalletType = await CliUx.ux.prompt([ 18 | '1. Create a new ethereum wallet', 19 | '2. I already have a private key', 20 | '' 21 | ].join('\n')) 22 | const password = await getPassword() 23 | const importing = newWalletType === '2' 24 | const privateKey = importing && await CliUx.ux.prompt('Enter your private key') 25 | 26 | const getMatic = await CliUx.ux.confirm('Do you want to get some free MATIC tokens from the Polygon MATIC Faucet? (recommended)') 27 | 28 | const cmd = this 29 | const list = new Listr([ 30 | { 31 | title: 'Create wallet', 32 | task: async (ctx, task) => { 33 | const wallet = await ( 34 | importing ? 35 | importWallet(password, privateKey) : 36 | createWallet(password) 37 | ) 38 | ctx.wallet = wallet 39 | const op = importing ? 'imported' : 'created' 40 | return `Wallet ${op}: ${wallet.address}` 41 | } 42 | }, 43 | { 44 | title: 'Check wallet balance', 45 | task: async (ctx, task) => { 46 | const startbal = await checkMaticBalance(ctx.wallet.address) 47 | ctx.startbal = startbal 48 | return `Wallet balance: ${startbal} MATIC` 49 | }, 50 | }, 51 | { 52 | title: 'Get MATIC from faucet?', 53 | skip: async () => { 54 | if (!getMatic) { 55 | return 'MATIC faucet skipped' 56 | } 57 | }, 58 | task: async (ctx, task) => { 59 | task.title = `MATIC tokens requested from Mumbai testnet faucet...` 60 | const res = await drinkFromFaucet(ctx.wallet.address) 61 | if (res.hash === 'TRANSACTION_SENT_TO_DB') { 62 | 63 | } else { 64 | throw new Error('Failed to request tokens from faucet, please try again in a few minutes') 65 | } 66 | return 'Drop requested. The MATIC may take a couple of minutes to show up in your account.' 67 | } 68 | }, 69 | { 70 | title: '', 71 | task: async (ctx, task) => { 72 | cmd.log(`MATIC tokens requested from Mumbai testnet faucet.`) 73 | cmd.log('The MATIC may take a couple of minutes to show up in your account.') 74 | cmd.log(`You can check your balance at https://mumbai.polygonscan.com/address/${ctx.wallet.address}`) 75 | } 76 | } 77 | ], 78 | {} 79 | ) 80 | 81 | list.run() 82 | } 83 | } 84 | 85 | async function getPassword(): Promise { 86 | const password = await CliUx.ux.prompt( 87 | 'Enter a password to encrypt the private key ' + 88 | '(You will not be able to recover the wallet if you forget this password)', 89 | { type: 'hide' } 90 | ) 91 | return password 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/cliux.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Flags } from '@oclif/core' 2 | import { EstuaryAPI, EstuaryCollection, EstuaryCollectionEntry } from './estuary' 3 | import treeify from 'treeify' 4 | import userConfig from '../config' 5 | import { login } from './wallet' 6 | 7 | // Given a wallet address, check if a collection exists 8 | // If it does, return the collection 9 | // If it doesn't, create a new collection and return it 10 | export async function getOrCreateCollection(address: string): Promise { 11 | let collection: any = userConfig.get('estuary.collection') 12 | const estuary = new EstuaryAPI() 13 | if (collection) { 14 | printCollection(collection) 15 | } else { 16 | const exists = await estuary.collectionExists(address) 17 | console.info(exists) 18 | if (!exists) { 19 | collection = await estuary.createCollection(address) 20 | userConfig.set('estuary.collection', collection) 21 | CliUx.ux.info(`Collection created: ${collection?.name}`) 22 | } 23 | printCollection(collection as EstuaryCollection) 24 | } 25 | return collection as EstuaryCollection 26 | } 27 | 28 | // Print the collection name and uuid of an EstuaryCollection 29 | export function printCollection(collection: EstuaryCollection) { 30 | CliUx.ux.info(`Using collection ${collection.name}`) 31 | CliUx.ux.info(`Collection UUID ${collection.uuid}`) 32 | } 33 | 34 | // Log an EstuaryCollectionEntry as an OCIF CLI UX table 35 | export async function estuaryFsTable( 36 | data: EstuaryCollectionEntry[], 37 | flags: CliUx.Table.table.Options 38 | ) { 39 | CliUx.ux.table( 40 | flatform(data), 41 | { 42 | name: { 43 | get: row => row.name 44 | }, 45 | path: { 46 | get: row => row.path 47 | }, 48 | cid: { 49 | get: row => row.cid, 50 | header: 'CID', 51 | minWidth: 40 52 | }, 53 | }, 54 | { 55 | printLine: l => console.log(l), 56 | ...flags 57 | } 58 | ) 59 | } 60 | 61 | // Log an EstuaryCollectionEntry as a treeify tree 62 | export async function estuaryFsTree(data: EstuaryCollectionEntry[]) { 63 | CliUx.ux.log(treeify.asTree(treeform(data), true, true)) 64 | } 65 | 66 | // Convert an array of Estuary collection entry descriptions 67 | // to a tree compatible with treeify 68 | export function treeform(data: EstuaryCollectionEntry[]) { 69 | const treedata: any = {} 70 | data.forEach(d => { 71 | if (d.children) treedata[d.name] = treeform(d.children) 72 | else if (d.cid) treedata[d.name] = d.cid 73 | }) 74 | return treedata 75 | } 76 | 77 | // Convert an array of Estuary collection entry descriptions 78 | // to a flat array compatible with OCLIF CLI UX table 79 | export function flatform(data: EstuaryCollectionEntry[]) { 80 | const flatdata: any = data.flatMap(d => { 81 | if (d.children) { 82 | return flatform(d.children) 83 | } else if (d.cid) { 84 | return d 85 | } 86 | return null 87 | }) 88 | return flatdata.filter((d: any) => d !== null) 89 | } 90 | 91 | // Boolean flag to force a command by skipping confirmation 92 | export const force = Flags.boolean({ 93 | char: 'f', 94 | description: 'Force submit job (if false, will prompt for confirmation)', 95 | env: 'OPENLAB_CLI_FORCE', 96 | }) 97 | 98 | // String flag for wallet password 99 | export const password = Flags.string({ 100 | char: 'p', 101 | description: 'Wallet password (if not supplied, will prompt for password)', 102 | env: 'OPENLAB_CLI_PASSWORD' 103 | }) 104 | 105 | export const globalFlags = { 106 | force, password 107 | } 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@labdao/openlab-cli", 3 | "version": "0.0.1", 4 | "description": "LabDAO OpenLab CLI", 5 | "author": "Rik Smith-Unna @blahah", 6 | "bin": { 7 | "openlab": "./bin/run" 8 | }, 9 | "homepage": "https://github.com/labdao/openlab-cli", 10 | "license": "MIT", 11 | "main": "dist/index.js", 12 | "repository": "labdao/openlab-cli", 13 | "files": [ 14 | "/bin", 15 | "/dist", 16 | "/npm-shrinkwrap.json", 17 | "/oclif.manifest.json", 18 | "/docs" 19 | ], 20 | "dependencies": { 21 | "@ensdomains/ensjs": "^3.0.0-alpha.2", 22 | "@ethersproject/providers": "^5.6.6", 23 | "@labdao/openlab-applayer-client": "^0.0.2", 24 | "@oclif/core": "^1", 25 | "@oclif/plugin-help": "^5", 26 | "apisauce": "^2.1.5", 27 | "async-folder-walker": "^2.2.1", 28 | "cfonts": "^3.1.0", 29 | "cids": "^1.1.9", 30 | "conf": "^10.1.2", 31 | "content-hash": "^2.5.2", 32 | "download": "^8.0.0", 33 | "ethers": "^5.6.6", 34 | "form-data": "^4.0.0", 35 | "get-ens": "^2.0.3", 36 | "inquirer": "^8.2.4", 37 | "isomorphic-fetch": "^3.0.0", 38 | "listr": "^0.14.3", 39 | "multiformats": "^9.7.0", 40 | "nunjucks": "^3.2.3", 41 | "tmp": "^0.2.1", 42 | "treeify": "^1.1.0", 43 | "web3": "^1.7.4", 44 | "web3-eth": "^1.7.4" 45 | }, 46 | "devDependencies": { 47 | "@oclif/test": "^2", 48 | "@types/chai": "^4", 49 | "@types/download": "^8.0.1", 50 | "@types/folder-walker": "^3.2.0", 51 | "@types/inquirer": "^8.2.1", 52 | "@types/isomorphic-fetch": "0.0.36", 53 | "@types/listr": "^0.14.4", 54 | "@types/mocha": "^9.1.1", 55 | "@types/node": "^16.9.4", 56 | "@types/tmp": "^0.2.3", 57 | "@types/treeify": "^1.0.0", 58 | "chai": "^4", 59 | "eslint": "^7.32.0", 60 | "globby": "^11", 61 | "mocha": "^9.2.2", 62 | "oclif": "^3", 63 | "oclif-plugin-helpdata": "^5.1.12", 64 | "shx": "^0.3.4", 65 | "ts-node": "^10.8.2", 66 | "tslib": "^2.4.0", 67 | "typescript": "^4.7.4" 68 | }, 69 | "oclif": { 70 | "bin": "openlab", 71 | "dirname": "openlab", 72 | "commands": "./dist/commands", 73 | "plugins": [ 74 | "@oclif/plugin-help", 75 | "oclif-plugin-helpdata" 76 | ], 77 | "topicSeparator": " ", 78 | "topics": { 79 | "account": { 80 | "description": "Connect and manage your Ethereum wallet" 81 | }, 82 | "app": { 83 | "description": "Discover and get information about available apps" 84 | }, 85 | "exchange": { 86 | "description": "Manage jobs on the OpenLab exchange" 87 | }, 88 | "file": { 89 | "description": "Manage files available to or from OpenLab on IPFS" 90 | } 91 | }, 92 | "hooks": { 93 | "init": "./src/hooks/init/load-from-ens.ts" 94 | } 95 | }, 96 | "scripts": { 97 | "build": "shx rm -rf tsconfig.tsbuildinfo dist && tsc -b", 98 | "docs": "npm run docs:mkjson && npm run docs:render", 99 | "docs:mkjson": "./bin/dev helpdata -n > docs/helpdata.json", 100 | "docs:readme": "oclif readme", 101 | "docs:render": "node docs/render.js", 102 | "lint": "eslint . --ext .ts --config .eslintrc", 103 | "postpack": "shx rm -f oclif.manifest.json", 104 | "posttest": "npm run lint", 105 | "prepack": "npm run build && oclif manifest && oclif readme", 106 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 107 | "test:nock-record": "ts-node ./scripts/nock-record.ts", 108 | "test:check-ens": "ts-node ./src/scripts/check-ens.ts" 109 | }, 110 | "engines": { 111 | "node": ">=16.0.0" 112 | }, 113 | "bugs": "https://github.com/labDAO/openlab-cli/issues", 114 | "keywords": [ 115 | "oclif" 116 | ], 117 | "types": "dist/index.d.ts" 118 | } 119 | -------------------------------------------------------------------------------- /src/lib/wallet.ts: -------------------------------------------------------------------------------- 1 | import fs, { fstatSync } from 'fs' 2 | import os from 'os' 3 | import path from 'path' 4 | 5 | import { AbiItem } from 'web3-utils' 6 | import axios from 'axios' 7 | import { CliUx } from '@oclif/core' 8 | import Web3 from 'web3' 9 | 10 | import { getERC20Contract, getToken } from './exchange/contracts' 11 | import constants from '../constants' 12 | import testUSDtokenABI from '../abis/testusd.json' 13 | 14 | const baseDir = path.join(os.homedir(), '.openlab') 15 | const walletPath = path.join(baseDir, 'wallet.json') 16 | const walletExists = fs.existsSync(walletPath) 17 | const provider = constants.provider.alchemyMumbai 18 | const web3 = new Web3(provider) 19 | 20 | // Load local wallet, unlock it and return the account 21 | export async function login(password?: string) { 22 | assertWalletExists() 23 | if (!password) { 24 | password = await CliUx.ux.prompt( 25 | 'Enter wallet password', 26 | { type: 'hide' } 27 | ) 28 | } 29 | const keystore = loadKeystore() 30 | const account = web3.eth.accounts.decrypt(keystore, password as string) 31 | web3.eth.accounts.wallet.add(account) 32 | return account 33 | } 34 | 35 | export function assertWalletExists() { 36 | if (!walletExists) { 37 | CliUx.ux.log('No wallet found!') 38 | CliUx.ux.log('You should create or add one by running:') 39 | CliUx.ux.log('> openlab account add') 40 | CliUx.ux.exit(1) 41 | } 42 | } 43 | 44 | export function loadKeystore() { 45 | assertWalletExists() 46 | const keystore = JSON.parse(fs.readFileSync(walletPath, 'utf-8')) 47 | return keystore 48 | } 49 | 50 | export async function createWallet( 51 | password: string, 52 | ) { 53 | const account = web3.eth.accounts.create() 54 | const encryptedAccount = web3.eth.accounts.encrypt( 55 | account.privateKey, 56 | password 57 | ) 58 | if (!fs.existsSync(path.dirname(walletPath))) { 59 | fs.mkdirSync(path.dirname(walletPath)) 60 | } 61 | fs.writeFileSync( 62 | walletPath, 63 | JSON.stringify(encryptedAccount) 64 | ) 65 | return account 66 | } 67 | 68 | export async function importWallet( 69 | password: string, 70 | privkey: string, 71 | ) { 72 | const account = web3.eth.accounts.privateKeyToAccount(privkey) 73 | const encryptedAccount = account.encrypt(password) 74 | 75 | fs.writeFileSync( 76 | walletPath, 77 | JSON.stringify(encryptedAccount) 78 | ) 79 | return account 80 | } 81 | 82 | export async function removeWallet() { 83 | fs.unlinkSync(walletPath) 84 | } 85 | 86 | export async function drinkFromFaucet(address: string) { 87 | const res = await axios.post( 88 | "https://api.faucet.matic.network/transferTokens", 89 | { 90 | network: "mumbai", 91 | address: address, 92 | token: "maticToken" 93 | } 94 | ) 95 | return res.data 96 | } 97 | 98 | // Get the MATIC balance of the wallet 99 | export async function checkMaticBalance(address: string): Promise { 100 | const res = await axios.get( 101 | 'https://api-testnet.polygonscan.com/api?module=account&action=balance&address=' 102 | + address 103 | ) 104 | const rawBalance = parseInt(res.data.result) 105 | const balance = web3.utils.fromWei(`${rawBalance}`) 106 | return balance 107 | } 108 | 109 | // Mint 100 test USD tokens 110 | export async function makeItRain(address: string): Promise { 111 | const testusdContract = new web3.eth.Contract( 112 | testUSDtokenABI as AbiItem[], 113 | getToken() 114 | ) 115 | const tx = await testusdContract.methods.mint().send( 116 | { 117 | from: address, 118 | gasLimit: 100000, 119 | gasPrice: web3.utils.toWei('30', 'gwei') 120 | } 121 | ) 122 | return tx 123 | } 124 | 125 | // Check ERC20 token balance 126 | export async function checkErc20Balance(address: string): Promise { 127 | const erc20Contract = await getERC20Contract() 128 | const rawbalance = await erc20Contract.methods.balanceOf( 129 | address 130 | ).call() 131 | const erc20Balance = web3.utils.fromWei(rawbalance) 132 | return erc20Balance 133 | } 134 | 135 | export default { 136 | login, 137 | createWallet, 138 | importWallet, 139 | removeWallet, 140 | drinkFromFaucet, 141 | checkMaticBalance, 142 | makeItRain, 143 | checkErc20Balance, 144 | } 145 | -------------------------------------------------------------------------------- /src/commands/job/submit.ts: -------------------------------------------------------------------------------- 1 | import { CliUx, Command, Flags } from '@oclif/core' 2 | import { login } from '../../lib/wallet' 3 | import { checkAllowance, submitJob } from '../../lib/exchange/contracts' 4 | import { EstuaryAPI } from '../../lib/estuary' 5 | import { globalFlags, getOrCreateCollection } from '../../lib/cliux' 6 | import Listr from 'listr' 7 | import { loadJobRequest } from '../../lib/job' 8 | 9 | export default class JobSubmit extends Command { 10 | static description = 'Submit a new job to lab-exchange' 11 | 12 | static flags = { 13 | force: globalFlags.force, 14 | password: globalFlags.password, 15 | jobPrice: Flags.string({ 16 | description: 'Price you will pay for the job (in gwei). If not specified, the default price will be used.', 17 | default: '10000' 18 | }), 19 | } 20 | 21 | static args = [ 22 | { 23 | name: 'request', 24 | description: 'Job request JSON file (local path, IPFS path, or IPFS CID)', 25 | required: true 26 | }, 27 | ] 28 | 29 | static examples = [ 30 | '<%= config.bin %> <%= command.id %>', 31 | ] 32 | public async run(): Promise { 33 | const { 34 | args, 35 | flags 36 | } = await this.parse(JobSubmit) 37 | console.log(flags) 38 | 39 | const account = await login(flags.password as string) 40 | const ctx = {} as any 41 | const tasks = new Listr([ 42 | { 43 | title: 'Confirm intent to submit job', 44 | task: async () => true, 45 | }, 46 | { 47 | title: 'Validate job request', 48 | task: async (ctx, task) => { 49 | const jobRequest = await loadJobRequest(args.request) 50 | if (!jobRequest) { 51 | throw new Error('Invalid job request') 52 | } else if (!jobRequest.inputs) { 53 | throw new Error('Job request must contain inputs') 54 | } else if (!jobRequest.appname) { 55 | throw new Error('Job request must contain appname') 56 | } 57 | ctx.jobRequest = jobRequest 58 | return `Validated: ${jobRequest.appname}` 59 | }, 60 | }, 61 | { 62 | title: 'Add job request to lab-exchange file storage network', 63 | task: async (ctx, task) => { 64 | const collection = await getOrCreateCollection(account.address) 65 | const estuary = new EstuaryAPI() 66 | 67 | // upload job request object to IPFS 68 | const requestUpload = await estuary.uploadObject( 69 | ctx.jobRequest, 'job-request' 70 | ) 71 | 72 | // upload metadata to IPFS 73 | ctx.requestMeta = { 74 | createdAt: Date.now(), 75 | requestedBy: account.address, 76 | request: '/ipfs/' + requestUpload.cid 77 | } 78 | const metaUpload = await estuary.uploadObject( 79 | ctx.requestMeta, 'job-request-meta' 80 | ) 81 | ctx.metaUpload = metaUpload 82 | 83 | // add metadata to virtual file system 84 | const request = await estuary.pushJobRequest( 85 | collection.uuid, 86 | ctx.metaUpload, 87 | ) 88 | ctx.requestCID = request.cid 89 | task.output = `Added job request to virtual file system: ${request.cid}` 90 | return true 91 | } 92 | }, 93 | { 94 | title: 'Check allowance', 95 | task: async (ctx, task) => { 96 | task.title = 'Checking allowance - waiting for contract response' 97 | task.output = flags.jobPrice 98 | ctx.allowanceStatus = await checkAllowance( 99 | flags.jobPrice, account 100 | ).catch( 101 | (err) => { 102 | throw new Error(err) 103 | } 104 | ) 105 | task.output = ctx.allowanceStatus 106 | return ctx.allowanceStatus 107 | } 108 | }, 109 | { 110 | title: 'Submit job to exchange contract', 111 | task: async (ctx, task) => { 112 | ctx.tx = await submitJob(account, flags.jobPrice, ctx.requestCID) 113 | return `Job submitted successfully! Transaction hash: ${ctx.tx}` 114 | } 115 | } 116 | ], {}) 117 | 118 | let confirm = true 119 | if (!flags.force) { 120 | confirm = await CliUx.ux.confirm( 121 | 'Are you sure you want to submit this job?' 122 | ) 123 | } 124 | tasks.run() 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/lib/exchange/contracts.ts: -------------------------------------------------------------------------------- 1 | import { Account, TransactionConfig } from 'web3-core' 2 | import { AbiItem, toBN } from 'web3-utils' 3 | import Web3 from 'web3' 4 | 5 | import wallet from '../wallet' 6 | 7 | import constants from '../../constants' 8 | import erc20ABI from '../../abis/erc20.json' 9 | import exchangeABI from '../../abis/exchange.json' 10 | 11 | const UINT256_MAX = '115792089237316195423570985008687907853269984665640564039457584007913129639935' 12 | 13 | const exchangeAddress = constants.contracts.maticMumbai.exchange 14 | const provider = constants.provider.alchemyMumbai 15 | const web3 = new Web3(provider) 16 | 17 | // Submit a new job to the exchange contract 18 | export async function submitJob( 19 | account: Account, 20 | jobCost: string, 21 | jobURI: string 22 | ) { 23 | const contract = getExchangeContract() 24 | const token = getToken() 25 | const jobCostWei = web3.utils.toWei(jobCost, 'gwei') 26 | 27 | const data = await contract.methods.submitJob( 28 | account.address, 29 | token, 30 | jobCostWei, 31 | jobURI 32 | ).encodeABI() 33 | 34 | const tx = await sendSignedTx(account, data) 35 | return tx 36 | } 37 | 38 | // Accept the job contract and return the transaction hash 39 | export async function acceptJob(account: Account, jobId: string) { 40 | const contract = getExchangeContract() 41 | const data = await contract.methods.acceptJob(jobId).encodeABI() 42 | const tx = await sendSignedTx(account, data) 43 | return tx 44 | } 45 | 46 | // Cancel the job and refund the client 47 | export async function refundJob(account: Account, jobId: string) { 48 | const contract = getExchangeContract() 49 | const data = await contract.methods.returnFunds(jobId).encodeABI() 50 | const tx = await sendSignedTx(account, data) 51 | return tx 52 | } 53 | 54 | // Complete the job contract with the final token swap 55 | // @param jobId - the job id to complete 56 | // @param tokenURI - the token URI to swap 57 | // @returns the transaction hash 58 | export async function completeContract( 59 | account: Account, 60 | jobId: string, 61 | tokenURI: string, 62 | ) { 63 | const contract = getExchangeContract() 64 | const data = await contract.methods.swap(jobId, tokenURI) 65 | const tx = await sendSignedTx(account, data) 66 | return tx 67 | } 68 | 69 | function standardContractParams(address: string) { 70 | return { 71 | from: address, 72 | gasLimit: 500000, 73 | gasPrice: web3.utils.toWei('30', 'gwei') 74 | } 75 | } 76 | 77 | function standardRawContractParams(data: string): TransactionConfig { 78 | return { 79 | to: exchangeAddress, 80 | gas: web3.utils.toHex(500000), 81 | data: data 82 | } 83 | } 84 | 85 | export async function sendSignedTx( 86 | from: Account, data: any 87 | ) { 88 | const txData: TransactionConfig = standardRawContractParams(data) 89 | const nonce = await web3.eth.getTransactionCount(from.address) 90 | const rawTx = Object.assign(txData, { 91 | from: from.address, nonce 92 | }) as TransactionConfig 93 | 94 | const signedTx = await web3.eth.accounts.signTransaction( 95 | rawTx, from.privateKey 96 | ) 97 | const tx = await web3.eth.sendSignedTransaction( 98 | signedTx.rawTransaction as string 99 | ) 100 | return tx 101 | } 102 | 103 | export function getExchangeContract() { 104 | return new web3.eth.Contract( 105 | exchangeABI as AbiItem[], exchangeAddress 106 | ) 107 | } 108 | 109 | export function getToken() { 110 | return constants.tokens.maticMumbai.USD 111 | } 112 | 113 | export async function getERC20Contract() { 114 | return new web3.eth.Contract( 115 | erc20ABI as AbiItem[], 116 | getToken() 117 | ) 118 | } 119 | 120 | // Get the balance of the token in the exchange contract 121 | export async function checkAllowance(jobCost: string, account?: Account) { 122 | if (!account) account = await wallet.login() 123 | const erc20Contract = await getERC20Contract() 124 | 125 | const allowance = await erc20Contract.methods.allowance( 126 | account.address, 127 | exchangeAddress 128 | ).call() 129 | 130 | 131 | const jobCostWei = web3.utils.toWei( 132 | toBN(jobCost), 'gwei' 133 | ) 134 | if (toBN(allowance).gt(jobCostWei)) { 135 | return 'Allowance already sufficient' 136 | } 137 | 138 | const tx = await erc20Contract.methods.approve( 139 | exchangeAddress, 140 | UINT256_MAX 141 | ).send( 142 | standardContractParams(account.address) 143 | ) 144 | return 'Exchange contract allowance approved: ' + tx.transactionHash 145 | } 146 | 147 | export default { 148 | submitJob, 149 | acceptJob, 150 | refundJob, 151 | completeContract, 152 | checkAllowance, 153 | getExchangeContract, 154 | getToken, 155 | getERC20Contract 156 | } 157 | -------------------------------------------------------------------------------- /src/abis/exchange.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[{"internalType":"address","name":"_factoryAddress","type":"address"},{"internalType":"address","name":"_factoryOwner","type":"address"},{"internalType":"address","name":"_openLabNFTAddress","type":"address"},{"internalType":"uint256","name":"_royaltyPercentage","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"","type":"address"},{"indexed":false,"internalType":"uint256","name":"","type":"uint256"}],"name":"Received","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"_jobId","type":"uint256"},{"indexed":true,"internalType":"address","name":"_client","type":"address"},{"indexed":true,"internalType":"address","name":"_provider","type":"address"},{"indexed":false,"internalType":"uint256","name":"_jobCost","type":"uint256"},{"indexed":false,"internalType":"string","name":"_jobURI","type":"string"},{"indexed":false,"internalType":"enum Exchange.JobStatus","name":"_status","type":"uint8"}],"name":"jobActive","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"_jobId","type":"uint256"},{"indexed":true,"internalType":"address","name":"_client","type":"address"},{"indexed":true,"internalType":"address","name":"_provider","type":"address"},{"indexed":false,"internalType":"uint256","name":"_jobCost","type":"uint256"},{"indexed":false,"internalType":"string","name":"_jobURI","type":"string"},{"indexed":false,"internalType":"enum Exchange.JobStatus","name":"_status","type":"uint8"}],"name":"jobCancelled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"_jobId","type":"uint256"},{"indexed":true,"internalType":"address","name":"_client","type":"address"},{"indexed":true,"internalType":"address","name":"_provider","type":"address"},{"indexed":false,"internalType":"uint256","name":"_jobCost","type":"uint256"},{"indexed":false,"internalType":"string","name":"_jobURI","type":"string"},{"indexed":false,"internalType":"enum Exchange.JobStatus","name":"_status","type":"uint8"},{"indexed":false,"internalType":"string","name":"_openLabNFTURI","type":"string"}],"name":"jobClosed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"_jobId","type":"uint256"},{"indexed":true,"internalType":"address","name":"_client","type":"address"},{"indexed":false,"internalType":"address","name":"_payableToken","type":"address"},{"indexed":false,"internalType":"uint256","name":"_jobCost","type":"uint256"},{"indexed":false,"internalType":"string","name":"_jobURI","type":"string"},{"indexed":false,"internalType":"enum Exchange.JobStatus","name":"_status","type":"uint8"}],"name":"jobCreated","type":"event"},{"inputs":[{"internalType":"uint256","name":"_jobId","type":"uint256"}],"name":"acceptJob","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_provider","type":"address"}],"name":"addValidatedProvider","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"clientAddresses","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"disableExchange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"factoryAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"factoryOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isEnabled","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"jobsList","outputs":[{"internalType":"address payable","name":"client","type":"address"},{"internalType":"address payable","name":"provider","type":"address"},{"internalType":"address","name":"payableToken","type":"address"},{"internalType":"uint256","name":"jobCost","type":"uint256"},{"internalType":"string","name":"jobURI","type":"string"},{"internalType":"enum Exchange.JobStatus","name":"status","type":"uint8"},{"internalType":"string","name":"openLabNFTURI","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"openLabNFTAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"providerAddresses","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"jobId","type":"uint256"}],"name":"returnFunds","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"royaltyBase","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"royaltyPercentage","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_percentage","type":"uint256"}],"name":"setRoyaltyPercentage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"_client","type":"address"},{"internalType":"address","name":"_payableToken","type":"address"},{"internalType":"uint256","name":"_jobCost","type":"uint256"},{"internalType":"string","name":"_jobURI","type":"string"}],"name":"submitJob","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"jobId","type":"uint256"},{"internalType":"string","name":"tokenURI","type":"string"}],"name":"swap","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}] 2 | -------------------------------------------------------------------------------- /src/abis/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "address", 13 | "name": "owner", 14 | "type": "address" 15 | }, 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "spender", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": false, 24 | "internalType": "uint256", 25 | "name": "value", 26 | "type": "uint256" 27 | } 28 | ], 29 | "name": "Approval", 30 | "type": "event" 31 | }, 32 | { 33 | "anonymous": false, 34 | "inputs": [ 35 | { 36 | "indexed": true, 37 | "internalType": "address", 38 | "name": "from", 39 | "type": "address" 40 | }, 41 | { 42 | "indexed": true, 43 | "internalType": "address", 44 | "name": "to", 45 | "type": "address" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "uint256", 50 | "name": "value", 51 | "type": "uint256" 52 | } 53 | ], 54 | "name": "Transfer", 55 | "type": "event" 56 | }, 57 | { 58 | "inputs": [ 59 | { 60 | "internalType": "address", 61 | "name": "owner", 62 | "type": "address" 63 | }, 64 | { 65 | "internalType": "address", 66 | "name": "spender", 67 | "type": "address" 68 | } 69 | ], 70 | "name": "allowance", 71 | "outputs": [ 72 | { 73 | "internalType": "uint256", 74 | "name": "", 75 | "type": "uint256" 76 | } 77 | ], 78 | "stateMutability": "view", 79 | "type": "function" 80 | }, 81 | { 82 | "inputs": [ 83 | { 84 | "internalType": "address", 85 | "name": "spender", 86 | "type": "address" 87 | }, 88 | { 89 | "internalType": "uint256", 90 | "name": "amount", 91 | "type": "uint256" 92 | } 93 | ], 94 | "name": "approve", 95 | "outputs": [ 96 | { 97 | "internalType": "bool", 98 | "name": "", 99 | "type": "bool" 100 | } 101 | ], 102 | "stateMutability": "nonpayable", 103 | "type": "function" 104 | }, 105 | { 106 | "inputs": [ 107 | { 108 | "internalType": "address", 109 | "name": "account", 110 | "type": "address" 111 | } 112 | ], 113 | "name": "balanceOf", 114 | "outputs": [ 115 | { 116 | "internalType": "uint256", 117 | "name": "", 118 | "type": "uint256" 119 | } 120 | ], 121 | "stateMutability": "view", 122 | "type": "function" 123 | }, 124 | { 125 | "inputs": [], 126 | "name": "decimals", 127 | "outputs": [ 128 | { 129 | "internalType": "uint8", 130 | "name": "", 131 | "type": "uint8" 132 | } 133 | ], 134 | "stateMutability": "view", 135 | "type": "function" 136 | }, 137 | { 138 | "inputs": [ 139 | { 140 | "internalType": "address", 141 | "name": "spender", 142 | "type": "address" 143 | }, 144 | { 145 | "internalType": "uint256", 146 | "name": "subtractedValue", 147 | "type": "uint256" 148 | } 149 | ], 150 | "name": "decreaseAllowance", 151 | "outputs": [ 152 | { 153 | "internalType": "bool", 154 | "name": "", 155 | "type": "bool" 156 | } 157 | ], 158 | "stateMutability": "nonpayable", 159 | "type": "function" 160 | }, 161 | { 162 | "inputs": [ 163 | { 164 | "internalType": "address", 165 | "name": "spender", 166 | "type": "address" 167 | }, 168 | { 169 | "internalType": "uint256", 170 | "name": "addedValue", 171 | "type": "uint256" 172 | } 173 | ], 174 | "name": "increaseAllowance", 175 | "outputs": [ 176 | { 177 | "internalType": "bool", 178 | "name": "", 179 | "type": "bool" 180 | } 181 | ], 182 | "stateMutability": "nonpayable", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [], 187 | "name": "name", 188 | "outputs": [ 189 | { 190 | "internalType": "string", 191 | "name": "", 192 | "type": "string" 193 | } 194 | ], 195 | "stateMutability": "view", 196 | "type": "function" 197 | }, 198 | { 199 | "inputs": [], 200 | "name": "symbol", 201 | "outputs": [ 202 | { 203 | "internalType": "string", 204 | "name": "", 205 | "type": "string" 206 | } 207 | ], 208 | "stateMutability": "view", 209 | "type": "function" 210 | }, 211 | { 212 | "inputs": [], 213 | "name": "totalSupply", 214 | "outputs": [ 215 | { 216 | "internalType": "uint256", 217 | "name": "", 218 | "type": "uint256" 219 | } 220 | ], 221 | "stateMutability": "view", 222 | "type": "function" 223 | }, 224 | { 225 | "inputs": [ 226 | { 227 | "internalType": "address", 228 | "name": "to", 229 | "type": "address" 230 | }, 231 | { 232 | "internalType": "uint256", 233 | "name": "amount", 234 | "type": "uint256" 235 | } 236 | ], 237 | "name": "transfer", 238 | "outputs": [ 239 | { 240 | "internalType": "bool", 241 | "name": "", 242 | "type": "bool" 243 | } 244 | ], 245 | "stateMutability": "nonpayable", 246 | "type": "function" 247 | }, 248 | { 249 | "inputs": [ 250 | { 251 | "internalType": "address", 252 | "name": "from", 253 | "type": "address" 254 | }, 255 | { 256 | "internalType": "address", 257 | "name": "to", 258 | "type": "address" 259 | }, 260 | { 261 | "internalType": "uint256", 262 | "name": "amount", 263 | "type": "uint256" 264 | } 265 | ], 266 | "name": "transferFrom", 267 | "outputs": [ 268 | { 269 | "internalType": "bool", 270 | "name": "", 271 | "type": "bool" 272 | } 273 | ], 274 | "stateMutability": "nonpayable", 275 | "type": "function" 276 | } 277 | ] 278 | -------------------------------------------------------------------------------- /src/abis/testusd.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "stateMutability": "nonpayable", 5 | "type": "constructor" 6 | }, 7 | { 8 | "anonymous": false, 9 | "inputs": [ 10 | { 11 | "indexed": true, 12 | "internalType": "address", 13 | "name": "owner", 14 | "type": "address" 15 | }, 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "spender", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": false, 24 | "internalType": "uint256", 25 | "name": "value", 26 | "type": "uint256" 27 | } 28 | ], 29 | "name": "Approval", 30 | "type": "event" 31 | }, 32 | { 33 | "anonymous": false, 34 | "inputs": [ 35 | { 36 | "indexed": true, 37 | "internalType": "address", 38 | "name": "from", 39 | "type": "address" 40 | }, 41 | { 42 | "indexed": true, 43 | "internalType": "address", 44 | "name": "to", 45 | "type": "address" 46 | }, 47 | { 48 | "indexed": false, 49 | "internalType": "uint256", 50 | "name": "value", 51 | "type": "uint256" 52 | } 53 | ], 54 | "name": "Transfer", 55 | "type": "event" 56 | }, 57 | { 58 | "inputs": [ 59 | { 60 | "internalType": "address", 61 | "name": "owner", 62 | "type": "address" 63 | }, 64 | { 65 | "internalType": "address", 66 | "name": "spender", 67 | "type": "address" 68 | } 69 | ], 70 | "name": "allowance", 71 | "outputs": [ 72 | { 73 | "internalType": "uint256", 74 | "name": "", 75 | "type": "uint256" 76 | } 77 | ], 78 | "stateMutability": "view", 79 | "type": "function" 80 | }, 81 | { 82 | "inputs": [ 83 | { 84 | "internalType": "address", 85 | "name": "spender", 86 | "type": "address" 87 | }, 88 | { 89 | "internalType": "uint256", 90 | "name": "amount", 91 | "type": "uint256" 92 | } 93 | ], 94 | "name": "approve", 95 | "outputs": [ 96 | { 97 | "internalType": "bool", 98 | "name": "", 99 | "type": "bool" 100 | } 101 | ], 102 | "stateMutability": "nonpayable", 103 | "type": "function" 104 | }, 105 | { 106 | "inputs": [ 107 | { 108 | "internalType": "address", 109 | "name": "account", 110 | "type": "address" 111 | } 112 | ], 113 | "name": "balanceOf", 114 | "outputs": [ 115 | { 116 | "internalType": "uint256", 117 | "name": "", 118 | "type": "uint256" 119 | } 120 | ], 121 | "stateMutability": "view", 122 | "type": "function" 123 | }, 124 | { 125 | "inputs": [], 126 | "name": "decimals", 127 | "outputs": [ 128 | { 129 | "internalType": "uint8", 130 | "name": "", 131 | "type": "uint8" 132 | } 133 | ], 134 | "stateMutability": "view", 135 | "type": "function" 136 | }, 137 | { 138 | "inputs": [ 139 | { 140 | "internalType": "address", 141 | "name": "spender", 142 | "type": "address" 143 | }, 144 | { 145 | "internalType": "uint256", 146 | "name": "subtractedValue", 147 | "type": "uint256" 148 | } 149 | ], 150 | "name": "decreaseAllowance", 151 | "outputs": [ 152 | { 153 | "internalType": "bool", 154 | "name": "", 155 | "type": "bool" 156 | } 157 | ], 158 | "stateMutability": "nonpayable", 159 | "type": "function" 160 | }, 161 | { 162 | "inputs": [ 163 | { 164 | "internalType": "address", 165 | "name": "spender", 166 | "type": "address" 167 | }, 168 | { 169 | "internalType": "uint256", 170 | "name": "addedValue", 171 | "type": "uint256" 172 | } 173 | ], 174 | "name": "increaseAllowance", 175 | "outputs": [ 176 | { 177 | "internalType": "bool", 178 | "name": "", 179 | "type": "bool" 180 | } 181 | ], 182 | "stateMutability": "nonpayable", 183 | "type": "function" 184 | }, 185 | { 186 | "inputs": [], 187 | "name": "mint", 188 | "outputs": [], 189 | "stateMutability": "nonpayable", 190 | "type": "function" 191 | }, 192 | { 193 | "inputs": [], 194 | "name": "name", 195 | "outputs": [ 196 | { 197 | "internalType": "string", 198 | "name": "", 199 | "type": "string" 200 | } 201 | ], 202 | "stateMutability": "view", 203 | "type": "function" 204 | }, 205 | { 206 | "inputs": [], 207 | "name": "symbol", 208 | "outputs": [ 209 | { 210 | "internalType": "string", 211 | "name": "", 212 | "type": "string" 213 | } 214 | ], 215 | "stateMutability": "view", 216 | "type": "function" 217 | }, 218 | { 219 | "inputs": [], 220 | "name": "totalSupply", 221 | "outputs": [ 222 | { 223 | "internalType": "uint256", 224 | "name": "", 225 | "type": "uint256" 226 | } 227 | ], 228 | "stateMutability": "view", 229 | "type": "function" 230 | }, 231 | { 232 | "inputs": [ 233 | { 234 | "internalType": "address", 235 | "name": "to", 236 | "type": "address" 237 | }, 238 | { 239 | "internalType": "uint256", 240 | "name": "amount", 241 | "type": "uint256" 242 | } 243 | ], 244 | "name": "transfer", 245 | "outputs": [ 246 | { 247 | "internalType": "bool", 248 | "name": "", 249 | "type": "bool" 250 | } 251 | ], 252 | "stateMutability": "nonpayable", 253 | "type": "function" 254 | }, 255 | { 256 | "inputs": [ 257 | { 258 | "internalType": "address", 259 | "name": "from", 260 | "type": "address" 261 | }, 262 | { 263 | "internalType": "address", 264 | "name": "to", 265 | "type": "address" 266 | }, 267 | { 268 | "internalType": "uint256", 269 | "name": "amount", 270 | "type": "uint256" 271 | } 272 | ], 273 | "name": "transferFrom", 274 | "outputs": [ 275 | { 276 | "internalType": "bool", 277 | "name": "", 278 | "type": "bool" 279 | } 280 | ], 281 | "stateMutability": "nonpayable", 282 | "type": "function" 283 | } 284 | ] 285 | -------------------------------------------------------------------------------- /docs/cli.api.json: -------------------------------------------------------------------------------- 1 | {"type":"root","topics":[{"description":"Discover and get information about available apps","name":"app"},{"description":"Manage files available to or from OpenLab on IPFS","name":"file"}],"commands":[{"id":"app","description":"get application details","strict":true,"pluginName":"@labdao/openlab-cli","pluginAlias":"@labdao/openlab-cli","pluginType":"core","aliases":[],"examples":["<%= config.bin %> <%= command.id %>"],"flags":{"json":{"name":"json","type":"boolean","description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false},"columns":{"name":"columns","type":"option","description":"only show provided columns (comma-separated)","multiple":false,"exclusive":["extended"]},"sort":{"name":"sort","type":"option","description":"property to sort by (prepend '-' for descending)","multiple":false},"filter":{"name":"filter","type":"option","description":"filter property by partial string matching, ex: name=foo","multiple":false},"csv":{"name":"csv","type":"boolean","description":"output is csv format [alias: --output=csv]","allowNo":false,"exclusive":["no-truncate"]},"output":{"name":"output","type":"option","description":"output in a more machine friendly format","multiple":false,"options":["csv","json","yaml"],"exclusive":["no-truncate","csv"]},"extended":{"name":"extended","type":"boolean","char":"x","description":"show extra columns","allowNo":false,"exclusive":["columns"]},"no-truncate":{"name":"no-truncate","type":"boolean","description":"do not truncate output to fit screen","allowNo":false,"exclusive":["csv"]},"no-header":{"name":"no-header","type":"boolean","description":"hide table header from output","allowNo":false,"exclusive":["csv"]}},"args":[{"name":"appname","description":"name of the application"}],"enableJsonFlag":true,"globalFlags":{"json":{"description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false,"type":"boolean"}}},{"id":"app:list","description":"list applications","strict":true,"pluginName":"@labdao/openlab-cli","pluginAlias":"@labdao/openlab-cli","pluginType":"core","aliases":[],"examples":["<%= config.bin %> <%= command.id %>"],"flags":{"json":{"name":"json","type":"boolean","description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false},"columns":{"name":"columns","type":"option","description":"only show provided columns (comma-separated)","multiple":false,"exclusive":["extended"]},"sort":{"name":"sort","type":"option","description":"property to sort by (prepend '-' for descending)","multiple":false},"filter":{"name":"filter","type":"option","description":"filter property by partial string matching, ex: name=foo","multiple":false},"csv":{"name":"csv","type":"boolean","description":"output is csv format [alias: --output=csv]","allowNo":false,"exclusive":["no-truncate"]},"output":{"name":"output","type":"option","description":"output in a more machine friendly format","multiple":false,"options":["csv","json","yaml"],"exclusive":["no-truncate","csv"]},"extended":{"name":"extended","type":"boolean","char":"x","description":"show extra columns","allowNo":false,"exclusive":["columns"]},"no-truncate":{"name":"no-truncate","type":"boolean","description":"do not truncate output to fit screen","allowNo":false,"exclusive":["csv"]},"no-header":{"name":"no-header","type":"boolean","description":"hide table header from output","allowNo":false,"exclusive":["csv"]}},"args":[{"name":"provider","description":"provider name or URL"}],"enableJsonFlag":true,"globalFlags":{"json":{"description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false,"type":"boolean"}}},{"id":"file:list","description":"list files","strict":true,"pluginName":"@labdao/openlab-cli","pluginAlias":"@labdao/openlab-cli","pluginType":"core","aliases":[],"examples":["<%= config.bin %> <%= command.id %>"],"flags":{"json":{"name":"json","type":"boolean","description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false},"columns":{"name":"columns","type":"option","description":"only show provided columns (comma-separated)","multiple":false,"exclusive":["extended"]},"sort":{"name":"sort","type":"option","description":"property to sort by (prepend '-' for descending)","multiple":false},"filter":{"name":"filter","type":"option","description":"filter property by partial string matching, ex: name=foo","multiple":false},"csv":{"name":"csv","type":"boolean","description":"output is csv format [alias: --output=csv]","allowNo":false,"exclusive":["no-truncate"]},"output":{"name":"output","type":"option","description":"output in a more machine friendly format","multiple":false,"options":["csv","json","yaml"],"exclusive":["no-truncate","csv"]},"extended":{"name":"extended","type":"boolean","char":"x","description":"show extra columns","allowNo":false,"exclusive":["columns"]},"no-truncate":{"name":"no-truncate","type":"boolean","description":"do not truncate output to fit screen","allowNo":false,"exclusive":["csv"]},"no-header":{"name":"no-header","type":"boolean","description":"hide table header from output","allowNo":false,"exclusive":["csv"]}},"args":[{"name":"path","description":"remote path to list"}],"enableJsonFlag":true,"globalFlags":{"json":{"description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false,"type":"boolean"}}},{"id":"file:pull","description":"pull a remote file from IPFS to your local file system","strict":true,"pluginName":"@labdao/openlab-cli","pluginAlias":"@labdao/openlab-cli","pluginType":"core","aliases":[],"examples":["<%= config.bin %> <%= command.id %> bafkreictm5biak56glcshkeungckjwf4tf33wxea566dozdyvhrrebnetu -o gp47_tail.fasta"],"flags":{"json":{"name":"json","type":"boolean","description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false},"outpath":{"name":"outpath","type":"option","char":"o","description":"the path where the pulled file or directory should be stored","multiple":false,"default":"[CID]"}},"args":[{"name":"CID","description":"the IPFS content identifier of the file or directory to pull","required":true}],"enableJsonFlag":true,"globalFlags":{"json":{"description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false,"type":"boolean"}}},{"id":"file:push","description":"push a local file from your storage system to IPFS","strict":true,"pluginName":"@labdao/openlab-cli","pluginAlias":"@labdao/openlab-cli","pluginType":"core","aliases":[],"examples":["<%= config.bin %> <%= command.id %>"],"flags":{"json":{"name":"json","type":"boolean","description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false}},"args":[{"name":"path","description":"path of file or directory to push"}],"enableJsonFlag":true,"globalFlags":{"json":{"description":"Format output as json.","helpGroup":"GLOBAL","allowNo":false,"type":"boolean"}}},{"id":"help","description":"Display help for <%= config.bin %>.","strict":false,"pluginName":"@oclif/plugin-help","pluginAlias":"@oclif/plugin-help","pluginType":"core","aliases":[],"flags":{"nested-commands":{"name":"nested-commands","type":"boolean","char":"n","description":"Include all nested commands in the output.","allowNo":false}},"args":[{"name":"command","description":"Command to show help for.","required":false}],"globalFlags":{},"examples":[]},{"id":"helpdata","description":"Emit help as structured data.","strict":false,"pluginName":"oclif-plugin-helpdata","pluginAlias":"oclif-plugin-helpdata","pluginType":"core","aliases":[],"flags":{"nested-commands":{"name":"nested-commands","type":"boolean","char":"n","description":"Include all nested commands in the output.","allowNo":false}},"args":[{"name":"command","description":"Command to show help for.","required":false}],"globalFlags":{},"examples":[]}]} -------------------------------------------------------------------------------- /src/lib/estuary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApisauceInstance, 3 | create as createAPI 4 | } from 'apisauce' 5 | import { 6 | createReadStream, writeFile, writeFileSync, 7 | } from 'fs' 8 | import path from 'path' 9 | import constants from '../constants' 10 | import { walkDirectory, FSItem } from './fs' 11 | import tmp from 'tmp' 12 | import FormData from 'form-data' 13 | 14 | interface EstuaryClientKey { 15 | token: string 16 | expiry: string 17 | } 18 | 19 | export interface EstuaryCollection { 20 | name: string 21 | uuid: string 22 | description: string 23 | createdAt: string 24 | userId: number 25 | } 26 | 27 | interface EstuaryPushResponse { 28 | cid: string 29 | estuaryId: number 30 | providers: string[] 31 | } 32 | 33 | export interface EstuaryListEntry { 34 | requestid: string 35 | status: string 36 | created: Date 37 | pin: EstuaryPin 38 | delegates: string[] 39 | info: null 40 | } 41 | 42 | export interface EstuaryCollectionEntry { 43 | name: string 44 | path: string 45 | type: string 46 | size: number 47 | contId: number 48 | cid?: string 49 | children?: EstuaryCollectionEntry[] 50 | } 51 | 52 | export interface EstuaryPin { 53 | cid: string 54 | name: string 55 | origins: null 56 | meta: null 57 | } 58 | 59 | type Entry = EstuaryListEntry | EstuaryCollectionEntry 60 | export interface EstuaryList { 61 | count: number 62 | results: Entry[] 63 | } 64 | 65 | export class EstuaryAPI { 66 | clientKey?: EstuaryClientKey 67 | api?: ApisauceInstance 68 | uploadApi?: ApisauceInstance 69 | uploadKeyApi?: ApisauceInstance 70 | 71 | constructor() { } 72 | 73 | async loadApiKey() { 74 | const uploadKeyApi = createAPI({ 75 | baseURL: constants.estuary.clientKeyUrl 76 | }) 77 | const res = await uploadKeyApi.post('/', {}) 78 | if (res.problem) { 79 | throw res.originalError 80 | } 81 | if (!res.data) throw new Error( 82 | 'Could not get Estuary API key - empty respose from issuer API' 83 | ) 84 | const data = res.data as EstuaryClientKey 85 | this.clientKey = data 86 | } 87 | 88 | async buildHeaders() { 89 | await this.loadApiKey() 90 | return { 91 | 'Authorization': this.clientKey ? 92 | 'Bearer ' + (this.clientKey as EstuaryClientKey).token : '' 93 | } 94 | } 95 | 96 | async getApi() { 97 | const headers = await this.buildHeaders() 98 | return createAPI({ 99 | baseURL: constants.estuary.estuaryApiUrl, 100 | headers: headers 101 | }) 102 | } 103 | 104 | async getUploadApi() { 105 | const headers = await this.buildHeaders() 106 | return createAPI({ 107 | baseURL: constants.estuary.estuaryUploadUrl, 108 | headers: headers 109 | }) 110 | } 111 | 112 | async listCollection(name: string) { 113 | const api = await this.getApi() 114 | const res = await api.get('collections/content/' + name) 115 | return res.data as EstuaryList 116 | } 117 | 118 | async collectionExists(name: string) { 119 | const api = await this.getApi() 120 | const res = await api.get('collections/' + name) 121 | return res.ok 122 | } 123 | 124 | async createCollection(name: string): Promise { 125 | const api = await this.getApi() 126 | const res = await api.post('collections/create', { 127 | name: name, 128 | }) 129 | if (!res.ok) return undefined 130 | return res.data as EstuaryCollection 131 | } 132 | 133 | async list(path: string): Promise { 134 | const api = await this.getApi() 135 | const res = await api.get('content/list') 136 | return res.data as EstuaryList 137 | } 138 | 139 | async getPin(pinid: string) { 140 | const api = await this.getApi() 141 | return api.get('pinning/pins/' + pinid) 142 | } 143 | 144 | async listCollectionFs(collection: string, remotepath: string) { 145 | const api = await this.getApi() 146 | let uri = `collections/fs/list?col=${collection}` 147 | if (remotepath) uri += `&dir=${remotepath}` 148 | const res = await api.get(uri) 149 | if (!res.ok) { 150 | throw new Error(`Could not list collection: ${collection} path: ${remotepath} (${JSON.stringify(res.data)})`) 151 | } 152 | const data = await Promise.all( 153 | (res.data as EstuaryCollectionEntry[]).map(async (entry) => { 154 | const path = `${remotepath}/${entry.name}`.replace('//', '/') 155 | entry.path = path 156 | if (entry.type === 'directory') { 157 | const children = await this.listCollectionFs(collection, path) 158 | if (children) entry.children = children 159 | } 160 | return entry 161 | }) 162 | ) 163 | return data as EstuaryCollectionEntry[] 164 | } 165 | 166 | async addFileToCollection( 167 | collection: string, estuaryId: number, remotepath: string 168 | ) { 169 | const api = await this.getApi() 170 | const res = await api.post( 171 | 'collections/fs/add', 172 | null, 173 | { 174 | params: { 175 | col: collection, 176 | content: estuaryId, 177 | path: remotepath 178 | } 179 | } 180 | ) 181 | if (!res.ok) { 182 | throw new Error( 183 | `Could not add file to collection: ${remotepath} (${res.data})` 184 | ) 185 | } 186 | return res.data 187 | } 188 | 189 | async uploadFile(filepath: string) { 190 | const form = new FormData() 191 | const abspath = path.normalize(filepath) 192 | form.append("data", createReadStream(abspath)) 193 | const uploadApi = await this.getUploadApi() 194 | const res = await uploadApi.post('content/add', form, { 195 | headers: { 196 | ...form.getHeaders() 197 | } 198 | }) 199 | if (!res.ok) { 200 | throw new Error(`Could not upload file: ${filepath} (${res.data})`) 201 | } 202 | const data = res.data as EstuaryPushResponse 203 | return data 204 | } 205 | 206 | async uploadObject(obj: any, name: string) { 207 | const form = new FormData() 208 | const tmpdir = tmp.dirSync({ unsafeCleanup: true }) 209 | const abspath = path.join(tmpdir.name, name + '.json') 210 | const json = JSON.stringify(obj) 211 | writeFileSync(abspath, json) 212 | form.append("data", createReadStream(abspath)) 213 | const uploadApi = await this.getUploadApi() 214 | const res = await uploadApi.post('content/add', form, { 215 | headers: { 216 | ...form.getHeaders() 217 | } 218 | }) 219 | if (!res.ok) { 220 | throw new Error(`Could not upload object: ${JSON.stringify(obj)} (${JSON.stringify(res.data)})`) 221 | } 222 | tmpdir.removeCallback() 223 | const data = res.data as EstuaryPushResponse 224 | return data 225 | } 226 | 227 | 228 | async pushDirectory( 229 | collection: string, dirpath: string, remotepath: string 230 | ) { 231 | walkDirectory(dirpath, async (entry: FSItem) => { 232 | const remoteRelPath = path.join(remotepath, entry.filepath) 233 | await this.pushFile(collection, entry.filepath, remoteRelPath) 234 | }) 235 | } 236 | 237 | async pushFile(collection: string, filepath: string, remotepath: string) { 238 | const uploadedFile = await this.uploadFile(filepath) 239 | const collectionEntry = await this.addFileToCollection( 240 | collection, uploadedFile.estuaryId, remotepath 241 | ) 242 | const listEntry = await this.listCollectionFs(collection, remotepath) 243 | 244 | return Object.assign( 245 | { 246 | collection, 247 | path: remotepath 248 | }, 249 | collectionEntry, 250 | listEntry, 251 | uploadedFile 252 | ) 253 | } 254 | 255 | async pushJobRequest(collection: string, uploadedFile: EstuaryPushResponse): Promise<{ collection: string; path: string } & EstuaryCollectionEntry[] & EstuaryPushResponse> { 256 | const remotepath = '/job_requests/by_cid/' + uploadedFile.cid + '.json' 257 | const collectionEntry = await this.addFileToCollection( 258 | collection, uploadedFile.estuaryId, remotepath 259 | ) 260 | const listEntry = await this.listCollectionFs(collection, remotepath) 261 | return Object.assign( 262 | { 263 | collection, 264 | path: remotepath 265 | }, 266 | collectionEntry, 267 | listEntry, 268 | uploadedFile 269 | ) 270 | } 271 | 272 | // async pullFile(cid: string, outpath: PathLike) { 273 | // const node = await createIPFSNode() 274 | // const iterable = node.get(CID.parse(cid)) 275 | // await writeIterableToFile(iterable, outpath) 276 | // } 277 | } 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

@labdao/openlab-cli 👋

2 | 3 |
4 | 5 | ![](https://flat.badgen.net/badge/icon/LabDAO?c&scale=2&icon=https://raw.githubusercontent.com/labdao/assets/main/logo/labdao_logo.svg&label) 6 | 7 | ![https://www.npmjs.com/package/@labdao/openlab-cli](https://img.shields.io/npm/v/@labdao/openlab-cli.svg?style=for-the-badge) 8 | ![https://img.shields.io/badge/node-%3E%3D16.0.0-blue.svg?style=for-the-badge&logo=node](https://img.shields.io/badge/node-%3E%3D16.0.0-blue.svg?style=for-the-badge&logo=node) 9 | ![[cli.openlab.tools](http://cli.openlab.tools)](https://img.shields.io/badge/documentation-cli.openlab.tools-brightgreen.svg?style=for-the-badge) 10 | !["License: MIT"](https://img.shields.io/badge/license-MIT-purple.svg?style=for-the-badge) 11 | 12 | ![open issues](https://flat.badgen.net/github/open-issues/labdao/openlab-cli) 13 | ![closed issues](https://flat.badgen.net/github/closed-issues/labdao/openlab-cli) 14 | ![dependabot](https://flat.badgen.net/github/dependabot/labdao/openlab-cli) 15 | 16 | ![discord](https://flat.badgen.net/discord/members/labdao?icon=discord) 17 | ![badge game](https://10q9gnv1kv6b.runkit.sh) 18 |
19 | 20 | ## What is this? 21 | 22 | `openlab-cli` is a command-line tool for the OpenLab ecosystem. 23 | 24 | It allows you to: 25 | 26 | - manage files stored in the OpenLab IPFS network 27 | - discover and explore available bioinformatics apps 28 | - run apps by creating and managing jobs 29 | 30 | ## When shoud I use this? 31 | 32 | When you want to interact with OpenLab from the command-line. 33 | 34 | ## How do I use this? 35 | 36 | * [Install](#install) 37 | * [Commands](#commands) 38 | 39 | ## Install 40 | 41 | ```bash 42 | npm install -g @labdao/openlab-cli 43 | ``` 44 | 45 | ## Commands 46 | 47 | 48 | - [`openlab account add`](#openlab-account-add) 49 | - [`openlab account address`](#openlab-account-address) 50 | - [`openlab account balance [TOKENSYMBOL]`](#openlab-account-balance-tokensymbol) 51 | - [`openlab account makeitrain`](#openlab-account-makeitrain) 52 | - [`openlab account remove`](#openlab-account-remove) 53 | - [`openlab app info [APPNAME]`](#openlab-app-info-appname) 54 | - [`openlab app list [PROVIDER]`](#openlab-app-list-provider) 55 | - [`openlab file list [PATH]`](#openlab-file-list-path) 56 | - [`openlab file pull CID`](#openlab-file-pull-cid) 57 | - [`openlab file push PATH [REMOTEPATH]`](#openlab-file-push-path-remotepath) 58 | - [`openlab job accept JOBID`](#openlab-job-accept-jobid) 59 | - [`openlab job complete JOBID TOKENURI`](#openlab-job-complete-jobid-tokenuri) 60 | - [`openlab job info JOBID`](#openlab-job-info-jobid) 61 | - [`openlab job list`](#openlab-job-list) 62 | - [`openlab job refund JOBID`](#openlab-job-refund-jobid) 63 | - [`openlab job submit REQUEST`](#openlab-job-submit-request) 64 | 65 | ### `openlab account add` 66 | 67 | Add an account by creating or importing an ETH wallet 68 | 69 | ``` 70 | USAGE 71 | $ openlab account add 72 | 73 | DESCRIPTION 74 | Add an account by creating or importing an ETH wallet 75 | 76 | ALIASES 77 | $ openlab wallet add 78 | 79 | EXAMPLES 80 | $ openlab account add 81 | ``` 82 | 83 | ### `openlab account address` 84 | 85 | Get the address of your local ETH wallet 86 | 87 | ``` 88 | USAGE 89 | $ openlab account address 90 | 91 | DESCRIPTION 92 | Get the address of your local ETH wallet 93 | 94 | EXAMPLES 95 | $ openlab account address 96 | ``` 97 | 98 | ### `openlab account balance [TOKENSYMBOL]` 99 | 100 | Get the balance of your local ETH wallet 101 | 102 | ``` 103 | USAGE 104 | $ openlab account balance [TOKENSYMBOL] [-p ] 105 | 106 | ARGUMENTS 107 | TOKENSYMBOL [default: USD] symbol of the ERC20 token 108 | 109 | FLAGS 110 | -p, --password= Wallet password (if not supplied, will prompt for password) 111 | 112 | DESCRIPTION 113 | Get the balance of your local ETH wallet 114 | 115 | ALIASES 116 | $ openlab wallet balance 117 | 118 | EXAMPLES 119 | $ openlab account balance 120 | ``` 121 | 122 | ### `openlab account makeitrain` 123 | 124 | Mint test USD tokens to your local ETH wallet 125 | 126 | ``` 127 | USAGE 128 | $ openlab account makeitrain [-p ] 129 | 130 | FLAGS 131 | -p, --password= Wallet password (if not supplied, will prompt for password) 132 | 133 | DESCRIPTION 134 | Mint test USD tokens to your local ETH wallet 135 | 136 | EXAMPLES 137 | $ openlab account makeitrain 138 | ``` 139 | 140 | ### `openlab account remove` 141 | 142 | Remove your local ETH wallet 143 | 144 | ``` 145 | USAGE 146 | $ openlab account remove 147 | 148 | DESCRIPTION 149 | Remove your local ETH wallet 150 | 151 | ALIASES 152 | $ openlab wallet remove 153 | 154 | EXAMPLES 155 | $ openlab account remove 156 | ``` 157 | 158 | ### `openlab app info [APPNAME]` 159 | 160 | Get the details of an application on lab-exchange 161 | 162 | ``` 163 | USAGE 164 | $ openlab app info [APPNAME] [--columns | -x] [--sort ] [--filter ] [--output 165 | csv|json|yaml | | [--csv | --no-truncate]] [--no-header | ] 166 | 167 | ARGUMENTS 168 | APPNAME Application name 169 | 170 | FLAGS 171 | -x, --extended show extra columns 172 | --columns= only show provided columns (comma-separated) 173 | --csv output is csv format [alias: --output=csv] 174 | --filter= filter property by partial string matching, ex: name=foo 175 | --no-header hide table header from output 176 | --no-truncate do not truncate output to fit screen 177 | --output=