Skip to content

feat(cli): add publish and mint using multicall #713

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 87 additions & 2 deletions cli/commands/contracts/gns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { logger } from '../../logging'
import { sendTransaction } from '../../network'
import { loadEnv, CLIArgs, CLIEnvironment } from '../../env'
import { nameToNode } from './ens'
import { IPFS, pinMetadataToIPFS } from '../../helpers'
import { IPFS, pinMetadataToIPFS, buildSubgraphID, ensureGRTAllowance } from '../../helpers'

export const setDefaultName = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<void> => {
const graphAccount = cliArgs.graphAccount
Expand Down Expand Up @@ -102,6 +102,44 @@ export const withdraw = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<v
await sendTransaction(cli.wallet, gns, 'withdraw', [subgraphID])
}

export const publishAndSignal = async (cli: CLIEnvironment, cliArgs: CLIArgs): Promise<void> => {
// parse args
const ipfs = cliArgs.ipfs
const subgraphDeploymentID = cliArgs.subgraphDeploymentID
const versionPath = cliArgs.versionPath
const subgraphPath = cliArgs.subgraphPath
const tokens = parseGRT(cliArgs.tokens)

// pin to IPFS
const subgraphDeploymentIDBytes = IPFS.ipfsHashToBytes32(subgraphDeploymentID)
const versionHashBytes = await pinMetadataToIPFS(ipfs, 'version', versionPath)
const subgraphHashBytes = await pinMetadataToIPFS(ipfs, 'subgraph', subgraphPath)

// craft transaction
const GNS = cli.contracts.GNS

// build publish tx
const publishTx = await GNS.populateTransaction.publishNewSubgraph(
subgraphDeploymentIDBytes,
versionHashBytes,
subgraphHashBytes,
)

// build mint tx
const subgraphID = buildSubgraphID(
cli.walletAddress,
await GNS.nextAccountSeqID(cli.walletAddress),
)
const mintTx = await GNS.populateTransaction.mintSignal(subgraphID, tokens, 0)

// ensure approval
await ensureGRTAllowance(cli.wallet, GNS.address, tokens, cli.contracts.GraphToken)

// send multicall transaction
logger.info(`Publishing and minting on new subgraph for ${cli.walletAddress}...`)
await sendTransaction(cli.wallet, GNS, 'multicall', [[publishTx.data, mintTx.data]])
}

export const gnsCommand = {
command: 'gns',
describe: 'GNS contract calls',
Expand Down Expand Up @@ -172,7 +210,7 @@ export const gnsCommand = {
})
.command({
command: 'publishNewVersion',
describe: 'Withdraw unlocked GRT',
describe: 'Publish a new subgraph version',
builder: (yargs: Argv) => {
return yargs
.option('subgraphID', {
Expand Down Expand Up @@ -307,6 +345,53 @@ export const gnsCommand = {
return withdraw(await loadEnv(argv), argv)
},
})
.command({
command: 'publishAndSignal',
describe: 'Publish a new subgraph and add initial signal',
builder: (yargs: Argv) => {
return yargs
.option('ipfs', {
description: 'ipfs endpoint. ex. https://api.thegraph.com/ipfs/',
type: 'string',
requiresArg: true,
demandOption: true,
})
.option('subgraphDeploymentID', {
description: 'subgraph deployment ID in base58',
type: 'string',
requiresArg: true,
demandOption: true,
})
.option('versionPath', {
description: ` filepath to metadata. With JSON format:\n
"description": "",
"label": ""`,
type: 'string',
requiresArg: true,
demandOption: true,
})
.option('subgraphPath', {
description: ` filepath to metadata. With JSON format:\n
"description": "",
"displayName": "",
"image": "",
"codeRepository": "",
"website": "",`,
type: 'string',
requiresArg: true,
demandOption: true,
})
.option('tokens', {
description: 'Amount of tokens to deposit',
type: 'string',
requiresArg: true,
demandOption: true,
})
},
handler: async (argv: CLIArgs): Promise<void> => {
return publishAndSignal(await loadEnv(argv), argv)
},
})
},
handler: (): void => {
yargs.showHelp()
Expand Down
37 changes: 32 additions & 5 deletions cli/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from 'fs'
import path from 'path'
import * as dotenv from 'dotenv'

import { utils, providers, Wallet } from 'ethers'
import { utils, BigNumber, BigNumberish, Signer } from 'ethers'
import ipfsHttpClient from 'ipfs-http-client'
import inquirer from 'inquirer'

Expand All @@ -15,6 +16,8 @@ import {
jsonToSubgraphMetadata,
jsonToVersionMetadata,
} from './metadata'
import { solidityKeccak256 } from 'ethers/lib/utils'
import { GraphToken } from '../build/types/GraphToken'

dotenv.config()

Expand Down Expand Up @@ -46,20 +49,24 @@ export class IPFS {
export const pinMetadataToIPFS = async (
ipfs: string,
type: string,
path?: string, // Only pass path or metadata, not both
filepath?: string, // Only pass path or metadata, not both
Copy link
Member

@tmigone tmigone Sep 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was curious if there was a ts trick to require only one of two properties:

type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>>
    & {
        [K in Keys]-?:
            Required<Pick<T, K>>
            & Partial<Record<Exclude<Keys, K>, undefined>>
    }[Keys]

I think we can stick with your code 😆

metadata?: SubgraphMetadata | VersionMetadata,
): Promise<string> => {
if (metadata == undefined && path != undefined) {
if (metadata == undefined && filepath != undefined) {
if (type == 'subgraph') {
metadata = jsonToSubgraphMetadata(JSON.parse(fs.readFileSync(__dirname + path).toString()))
metadata = jsonToSubgraphMetadata(
JSON.parse(fs.readFileSync(path.join(__dirname, filepath)).toString()),
)
logger.info('Meta data:')
logger.info(` Subgraph Description: ${metadata.description}`)
logger.info(`Subgraph Display Name: ${metadata.displayName}`)
logger.info(` Subgraph Image: ${metadata.image}`)
logger.info(` Subgraph Code Repository: ${metadata.codeRepository}`)
logger.info(` Subgraph Website: ${metadata.website}`)
} else if (type == 'version') {
metadata = jsonToVersionMetadata(JSON.parse(fs.readFileSync(__dirname + path).toString()))
metadata = jsonToVersionMetadata(
JSON.parse(fs.readFileSync(path.join(__dirname, filepath)).toString()),
)
logger.info('Meta data:')
logger.info(` Version Description: ${metadata.description}`)
logger.info(` Version Label: ${metadata.label}`)
Expand Down Expand Up @@ -104,3 +111,23 @@ export const confirm = async (message: string, skip: boolean): Promise<boolean>
}
return true
}

export const buildSubgraphID = (account: string, seqID: BigNumber): string =>
solidityKeccak256(['address', 'uint256'], [account, seqID])

export const ensureGRTAllowance = async (
owner: Signer,
spender: string,
amount: BigNumberish,
grt: GraphToken,
): Promise<void> => {
const ownerAddress = await owner.getAddress()
const allowance = await grt.allowance(ownerAddress, spender)
const allowTokens = BigNumber.from(amount).sub(allowance)
if (allowTokens.gt(0)) {
console.log(
`\nApproving ${spender} to spend ${allowTokens} tokens on ${ownerAddress} behalf...`,
)
await grt.connect(owner).approve(spender, amount)
}
}