@graphprotocol/graph-cli
Version:
CLI for building for and deploying to The Graph
1,008 lines (997 loc) • 41 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const gluegun_1 = require("gluegun");
const toolbox = __importStar(require("gluegun"));
const core_1 = require("@oclif/core");
const abi_1 = require("../command-helpers/abi");
const network_1 = require("../command-helpers/network");
const node_1 = require("../command-helpers/node");
const scaffold_1 = require("../command-helpers/scaffold");
const spinner_1 = require("../command-helpers/spinner");
const subgraph_1 = require("../command-helpers/subgraph");
const constants_1 = require("../constants");
const debug_1 = __importDefault(require("../debug"));
const protocols_1 = __importDefault(require("../protocols"));
const schema_1 = require("../scaffold/schema");
const validation_1 = require("../validation");
const add_1 = __importDefault(require("./add"));
const protocolChoices = Array.from(protocols_1.default.availableProtocols().keys());
const initDebugger = (0, debug_1.default)('graph-cli:commands:init');
/**
* a dynamic list of available networks supported by the studio
*/
const AVAILABLE_NETWORKS = async () => {
const logger = initDebugger.extend('AVAILABLE_NETWORKS');
try {
logger('fetching chain_list from studio');
const res = await fetch(node_1.SUBGRAPH_STUDIO_URL, {
method: 'POST',
headers: {
...constants_1.GRAPH_CLI_SHARED_HEADERS,
'content-type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'chain_list',
params: [],
}),
});
if (!res.ok) {
logger("Something went wrong while fetching 'chain_list' from studio HTTP code: %o", res.status);
return null;
}
const result = await res.json();
if (result?.result) {
logger('chain_list result: %o', result.result);
return result.result;
}
logger("Unable to get result for 'chain_list' from studio: %O", result);
return null;
}
catch (e) {
logger('error: %O', e);
return null;
}
};
const DEFAULT_EXAMPLE_SUBGRAPH = 'ethereum-gravatar';
class InitCommand extends core_1.Command {
async run() {
const { args: { subgraphName, directory }, flags, } = await this.parse(InitCommand);
const { protocol, product, studio, node: nodeFlag, 'allow-simple-name': allowSimpleNameFlag, 'from-contract': fromContract, 'contract-name': contractName, 'from-example': fromExample, 'index-events': indexEvents, 'skip-install': skipInstall, 'skip-git': skipGit, network, abi: abiPath, 'start-block': startBlock, spkg: spkgPath, } = flags;
initDebugger('Flags: %O', flags);
let { node, allowSimpleName } = (0, node_1.chooseNodeUrl)({
product,
// if we are loading example, we want to ensure we are using studio
studio: studio || fromExample !== undefined,
node: nodeFlag,
allowSimpleName: allowSimpleNameFlag,
});
if (fromContract && fromExample) {
this.error('Only one of "--from-example" and "--from-contract" can be used at a time.', {
exit: 1,
});
}
// Detect git
const git = gluegun_1.system.which('git');
if (!git) {
this.error('Git was not found on your system. Please install "git" so it is in $PATH.', {
exit: 1,
});
}
// Detect Yarn and/or NPM
const yarn = gluegun_1.system.which('yarn');
const npm = gluegun_1.system.which('npm');
if (!yarn && !npm) {
this.error(`Neither Yarn nor NPM were found on your system. Please install one of them.`, {
exit: 1,
});
}
const commands = {
link: yarn ? 'yarn link @graphprotocol/graph-cli' : 'npm link @graphprotocol/graph-cli',
install: yarn ? 'yarn' : 'npm install',
codegen: yarn ? 'yarn codegen' : 'npm run codegen',
deploy: yarn ? 'yarn deploy' : 'npm run deploy',
};
// If all parameters are provided from the command-line,
// go straight to creating the subgraph from the example
if (fromExample && subgraphName && directory) {
await initSubgraphFromExample.bind(this)({
fromExample,
allowSimpleName,
directory,
subgraphName,
skipInstall,
skipGit,
}, { commands });
// Exit with success
return this.exit(0);
}
// Will be assigned below if ethereum
let abi;
// If all parameters are provided from the command-line,
// go straight to creating the subgraph from an existing contract
if (fromContract && protocol && subgraphName && directory && network && node) {
if (!protocolChoices.includes(protocol)) {
this.error(`Protocol '${protocol}' is not supported, choose from these options: ${protocolChoices.join(', ')}`, { exit: 1 });
}
const protocolInstance = new protocols_1.default(protocol);
if (protocolInstance.hasABIs()) {
const ABI = protocolInstance.getABI();
if (abiPath) {
try {
abi = loadAbiFromFile(ABI, abiPath);
}
catch (e) {
this.error(`Failed to load ABI: ${e.message}`, { exit: 1 });
}
}
else {
try {
if (network === 'poa-core') {
abi = await (0, abi_1.loadAbiFromBlockScout)(ABI, network, fromContract);
}
else {
abi = await (0, abi_1.loadAbiFromEtherscan)(ABI, network, fromContract);
}
}
catch (e) {
process.exitCode = 1;
return;
}
}
}
await initSubgraphFromContract.bind(this)({
protocolInstance,
abi,
allowSimpleName,
directory,
contract: fromContract,
indexEvents,
network,
subgraphName,
contractName,
node,
startBlock,
spkgPath,
skipInstall,
skipGit,
}, { commands, addContract: false });
// Exit with success
return this.exit(0);
}
if (fromExample) {
const answers = await processFromExampleInitForm.bind(this)({
allowSimpleName,
subgraphName,
directory,
});
if (!answers) {
this.exit(1);
return;
}
await initSubgraphFromExample.bind(this)({
allowSimpleName,
fromExample,
subgraphName: answers.subgraphName,
directory: answers.directory,
skipInstall,
skipGit,
}, { commands });
}
else {
// Otherwise, take the user through the interactive form
const answers = await processInitForm.bind(this)({
protocol: protocol,
product,
studio,
node,
abi,
abiPath,
allowSimpleName,
directory,
contract: fromContract,
indexEvents,
fromExample,
network,
subgraphName,
contractName,
startBlock,
spkgPath,
});
if (!answers) {
this.exit(1);
return;
}
({ node, allowSimpleName } = (0, node_1.chooseNodeUrl)({
product: answers.product,
studio: answers.studio,
node,
allowSimpleName,
}));
await initSubgraphFromContract.bind(this)({
protocolInstance: answers.protocolInstance,
allowSimpleName,
subgraphName: answers.subgraphName,
directory: answers.directory,
abi: answers.abi,
network: answers.network,
contract: answers.contract,
indexEvents: answers.indexEvents,
contractName: answers.contractName,
node,
startBlock: answers.startBlock,
spkgPath: answers.spkgPath,
skipInstall,
skipGit,
}, { commands, addContract: true });
}
// Exit with success
this.exit(0);
}
}
InitCommand.description = 'Creates a new subgraph with basic scaffolding.';
InitCommand.args = {
subgraphName: core_1.Args.string(),
directory: core_1.Args.string(),
};
InitCommand.flags = {
help: core_1.Flags.help({
char: 'h',
}),
protocol: core_1.Flags.string({
options: protocolChoices,
}),
product: core_1.Flags.string({
summary: 'Selects the product for which to initialize.',
options: ['subgraph-studio', 'hosted-service'],
deprecated: {
message: 'In next major version, this flag will be removed. By default we will deploy to the Graph Studio. Learn more about Sunrise of Decentralized Data https://thegraph.com/blog/unveiling-updated-sunrise-decentralized-data/',
},
}),
studio: core_1.Flags.boolean({
summary: 'Shortcut for "--product subgraph-studio".',
exclusive: ['product'],
deprecated: {
message: 'In next major version, this flag will be removed. By default we will deploy to the Graph Studio. Learn more about Sunrise of Decentralized Data https://thegraph.com/blog/unveiling-updated-sunrise-decentralized-data/',
},
}),
node: core_1.Flags.string({
summary: 'Graph node for which to initialize.',
char: 'g',
}),
'allow-simple-name': core_1.Flags.boolean({
description: 'Use a subgraph name without a prefix.',
default: false,
deprecated: {
message: 'In next major version, this flag will be removed. By default we will deploy to the Graph Studio. Learn more about Sunrise of Decentralized Data https://thegraph.com/blog/unveiling-updated-sunrise-decentralized-data/',
},
}),
'from-contract': core_1.Flags.string({
description: 'Creates a scaffold based on an existing contract.',
exclusive: ['from-example'],
}),
'from-example': core_1.Flags.string({
description: 'Creates a scaffold based on an example subgraph.',
// TODO: using a default sets the value and therefore requires not to have --from-contract
// default: 'Contract',
exclusive: ['from-contract'],
}),
'contract-name': core_1.Flags.string({
helpGroup: 'Scaffold from contract',
description: 'Name of the contract.',
dependsOn: ['from-contract'],
}),
'index-events': core_1.Flags.boolean({
helpGroup: 'Scaffold from contract',
description: 'Index contract events as entities.',
dependsOn: ['from-contract'],
}),
'skip-install': core_1.Flags.boolean({
summary: 'Skip installing dependencies.',
default: false,
}),
'skip-git': core_1.Flags.boolean({
summary: 'Skip initializing a Git repository.',
default: false,
deprecated: {
message: 'In next major version, this flag will be removed. By default we will stop initializing a Git repository.',
},
}),
'start-block': core_1.Flags.string({
helpGroup: 'Scaffold from contract',
description: 'Block number to start indexing from.',
// TODO: using a default sets the value and therefore requires --from-contract
// default: '0',
dependsOn: ['from-contract'],
}),
abi: core_1.Flags.string({
summary: 'Path to the contract ABI',
// TODO: using a default sets the value and therefore requires --from-contract
// default: '*Download from Etherscan*',
dependsOn: ['from-contract'],
}),
spkg: core_1.Flags.string({
summary: 'Path to the SPKG file',
}),
network: core_1.Flags.string({
summary: 'Network the contract is deployed to.',
description: 'Check https://thegraph.com/docs/en/developing/supported-networks/ for supported networks',
dependsOn: ['from-contract'],
}),
};
exports.default = InitCommand;
async function processFromExampleInitForm({ directory: initDirectory, subgraphName: initSubgraphName, allowSimpleName: initAllowSimpleName, }) {
try {
const { subgraphName } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'subgraphName',
// TODO: is defaulting to studio ok?
message: () => 'Subgraph slug',
initial: initSubgraphName,
validate: name => {
try {
(0, subgraph_1.validateSubgraphName)(name, {
allowSimpleName: initAllowSimpleName,
});
return true;
}
catch (e) {
return `${e.message}
Examples:
$ graph init ${os_1.default.userInfo().username}/${name}
$ graph init ${name} --allow-simple-name`;
}
},
},
]);
const { directory } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'directory',
message: 'Directory to create the subgraph in',
initial: () => initDirectory || (0, subgraph_1.getSubgraphBasename)(subgraphName),
validate: value => gluegun_1.filesystem.exists(value || initDirectory || (0, subgraph_1.getSubgraphBasename)(subgraphName))
? 'Directory already exists'
: true,
},
]);
return {
subgraphName,
directory,
};
}
catch (e) {
this.error(e, { exit: 1 });
}
}
async function retryWithPrompt(func) {
for (;;) {
try {
return await func();
}
catch (_) {
const { retry } = await toolbox.prompt.ask({
type: 'confirm',
name: 'retry',
message: 'Do you want to retry?',
initial: true,
});
if (!retry) {
break;
}
}
}
return undefined;
}
async function processInitForm({ protocol: initProtocol, product: initProduct, studio: initStudio, node: initNode, abi: initAbi, abiPath: initAbiPath, directory: initDirectory, contract: initContract, indexEvents: initIndexEvents, fromExample: initFromExample, network: initNetwork, subgraphName: initSubgraphName, contractName: initContractName, startBlock: initStartBlock, allowSimpleName: initAllowSimpleName, spkgPath: initSpkgPath, }) {
let abiFromEtherscan = undefined;
try {
const { protocol } = await gluegun_1.prompt.ask({
type: 'select',
name: 'protocol',
message: 'Protocol',
choices: protocolChoices,
skip: protocolChoices.includes(String(initProtocol)),
result: value => {
if (initProtocol) {
initDebugger.extend('processInitForm')('initProtocol: %O', initProtocol);
return initProtocol;
}
initDebugger.extend('processInitForm')('protocol: %O', value);
return value;
},
});
const protocolInstance = new protocols_1.default(protocol);
const isSubstreams = protocol === 'substreams';
initDebugger.extend('processInitForm')('isSubstreams: %O', isSubstreams);
const { product } = await gluegun_1.prompt.ask([
{
type: 'select',
name: 'product',
message: 'Product for which to initialize',
choices: ['subgraph-studio', 'hosted-service'],
skip: protocol === 'arweave' ||
protocol === 'cosmos' ||
protocol === 'near' ||
initProduct === 'subgraph-studio' ||
initProduct === 'hosted-service' ||
initStudio !== undefined ||
initNode !== undefined,
result: value => {
if (initProduct)
return initProduct;
if (initStudio)
return 'subgraph-studio';
// For now we only support NEAR subgraphs in the Hosted Service
if (protocol === 'near') {
return 'hosted-service';
}
if (value == 'subgraph-studio') {
initAllowSimpleName = true;
}
return value;
},
},
]);
const { subgraphName } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'subgraphName',
message: () => (product == 'subgraph-studio' ? 'Subgraph slug' : 'Subgraph name'),
initial: initSubgraphName,
validate: name => {
try {
(0, subgraph_1.validateSubgraphName)(name, {
allowSimpleName: initAllowSimpleName,
});
return true;
}
catch (e) {
return `${e.message}
Examples:
$ graph init ${os_1.default.userInfo().username}/${name}
$ graph init ${name} --allow-simple-name`;
}
},
},
]);
const { directory } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'directory',
message: 'Directory to create the subgraph in',
initial: () => initDirectory || (0, subgraph_1.getSubgraphBasename)(subgraphName),
validate: value => gluegun_1.filesystem.exists(value || initDirectory || (0, subgraph_1.getSubgraphBasename)(subgraphName))
? 'Directory already exists'
: true,
},
]);
const choices = (await AVAILABLE_NETWORKS())?.[product === 'subgraph-studio' ? 'studio' : 'hostedService'];
if (!choices) {
this.error('Unable to fetch available networks from API. Please report this issue. As a workaround you can pass `--network` flag from the available networks: https://thegraph.com/docs/en/developing/supported-networks', { exit: 1 });
}
const { network } = await gluegun_1.prompt.ask([
{
type: 'select',
name: 'network',
message: () => `${protocolInstance.displayName()} network`,
choices,
skip: initNetwork !== undefined,
result: value => {
if (initNetwork) {
initDebugger.extend('processInitForm')('initNetwork: %O', initNetwork);
return initNetwork;
}
initDebugger.extend('processInitForm')('network: %O', value);
return value;
},
},
]);
const { contract } = await gluegun_1.prompt.ask([
// TODO:
// protocols that don't support contract
// - arweave
// - cosmos
{
type: 'input',
name: 'contract',
message: `Contract ${protocolInstance.getContract()?.identifierName()}`,
skip: () => initFromExample !== undefined || !protocolInstance.hasContract() || isSubstreams,
initial: initContract,
validate: async (value) => {
if (initFromExample !== undefined || !protocolInstance.hasContract()) {
return true;
}
const protocolContract = protocolInstance.getContract();
if (!protocolContract) {
return 'Contract not found.';
}
// Validate whether the contract is valid
const { valid, error } = (0, validation_1.validateContract)(value, protocolContract);
return valid ? true : error;
},
result: async (value) => {
if (initFromExample !== undefined || isSubstreams || initAbiPath) {
return value;
}
const ABI = protocolInstance.getABI();
// Try loading the ABI from Etherscan, if none was provided
if (protocolInstance.hasABIs() && !initAbi) {
if (network === 'poa-core') {
abiFromEtherscan = await retryWithPrompt(() => (0, abi_1.loadAbiFromBlockScout)(ABI, network, value));
}
else {
abiFromEtherscan = await retryWithPrompt(() => (0, abi_1.loadAbiFromEtherscan)(ABI, network, value));
}
}
// If startBlock is not set, try to load it.
if (!initStartBlock) {
// Load startBlock for this contract
const startBlock = await retryWithPrompt(() => (0, abi_1.loadStartBlockForContract)(network, value));
if (startBlock) {
initStartBlock = Number(startBlock).toString();
}
}
return value;
},
},
]);
const { spkg } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'spkg',
message: 'SPKG file (path)',
initial: () => initSpkgPath,
skip: () => !isSubstreams || !!initSpkgPath,
validate: value => gluegun_1.filesystem.exists(initSpkgPath || value) ? true : 'SPKG file does not exist',
},
]);
const { abi: abiFromFile } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'abi',
message: 'ABI file (path)',
initial: initAbi,
skip: () => !protocolInstance.hasABIs() ||
initFromExample !== undefined ||
abiFromEtherscan !== undefined ||
isSubstreams ||
!!initAbiPath,
validate: async (value) => {
if (initFromExample || abiFromEtherscan || !protocolInstance.hasABIs()) {
return true;
}
const ABI = protocolInstance.getABI();
if (initAbiPath) {
try {
loadAbiFromFile(ABI, initAbiPath);
return true;
}
catch (e) {
this.error(e.message);
}
}
try {
loadAbiFromFile(ABI, value);
return true;
}
catch (e) {
this.error(e.message);
}
},
result: async (value) => {
if (initFromExample || abiFromEtherscan || !protocolInstance.hasABIs()) {
return null;
}
const ABI = protocolInstance.getABI();
if (initAbiPath) {
try {
return loadAbiFromFile(ABI, initAbiPath);
}
catch (e) {
return e.message;
}
}
try {
return loadAbiFromFile(ABI, value);
}
catch (e) {
return e.message;
}
},
},
]);
const { startBlock } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'startBlock',
message: 'Start Block',
initial: initStartBlock || '0',
skip: () => initFromExample !== undefined || isSubstreams,
validate: value => parseInt(value) >= 0,
result(value) {
if (initStartBlock)
return initStartBlock;
return value;
},
},
]);
const { contractName } = await gluegun_1.prompt.ask([
{
type: 'input',
name: 'contractName',
message: 'Contract Name',
initial: initContractName || 'Contract' || isSubstreams,
skip: () => initFromExample !== undefined || !protocolInstance.hasContract(),
validate: value => value && value.length > 0,
},
]);
const { indexEvents } = await gluegun_1.prompt.ask([
{
type: 'confirm',
name: 'indexEvents',
message: 'Index contract events as entities',
initial: true,
skip: () => !!initIndexEvents || isSubstreams,
},
]);
return {
abi: abiFromEtherscan || abiFromFile,
protocolInstance,
subgraphName,
directory,
studio: product === 'subgraph-studio',
startBlock,
fromExample: !!initFromExample,
product,
network,
contractName,
contract,
indexEvents,
spkgPath: spkg,
};
}
catch (e) {
this.error(e, { exit: 1 });
}
}
const loadAbiFromFile = (ABI, filename) => {
const exists = gluegun_1.filesystem.exists(filename);
if (!exists) {
throw Error('File does not exist.');
}
else if (exists === 'dir') {
throw Error('Path points to a directory, not a file.');
}
else if (exists === 'other') {
throw Error('Not sure what this path points to.');
}
else {
return ABI.load('Contract', filename);
}
};
function revalidateSubgraphName(subgraphName, { allowSimpleName }) {
// Fail if the subgraph name is invalid
try {
(0, subgraph_1.validateSubgraphName)(subgraphName, { allowSimpleName });
return true;
}
catch (e) {
this.error(`${e.message}
Examples:
$ graph init ${os_1.default.userInfo().username}/${subgraphName}
$ graph init ${subgraphName} --allow-simple-name`);
}
}
// Inspired from: https://github.com/graphprotocol/graph-tooling/issues/1450#issuecomment-1713992618
async function isInRepo() {
try {
const result = await gluegun_1.system.run('git rev-parse --is-inside-work-tree');
// It seems like we are returning "true\n" instead of "true".
// Don't think it is great idea to check for new line character here.
// So best to just check if the result includes "true".
return result.includes('true');
}
catch (err) {
if (err.stderr.includes('not a git repository')) {
return false;
}
throw Error(err.stderr);
}
}
const initRepository = async (directory) => await (0, spinner_1.withSpinner)(`Initialize subgraph repository`, `Failed to initialize subgraph repository`, `Warnings while initializing subgraph repository`, async () => {
// Remove .git dir in --from-example mode; in --from-contract, we're
// starting from an empty directory
const gitDir = path_1.default.join(directory, '.git');
if (gluegun_1.filesystem.exists(gitDir)) {
gluegun_1.filesystem.remove(gitDir);
}
if (await isInRepo()) {
await gluegun_1.system.run('git add --all', { cwd: directory });
await gluegun_1.system.run('git commit -m "Initialize subgraph"', {
cwd: directory,
});
}
else {
await gluegun_1.system.run('git init', { cwd: directory });
await gluegun_1.system.run('git add --all', { cwd: directory });
await gluegun_1.system.run('git commit -m "Initial commit"', {
cwd: directory,
});
}
return true;
});
const installDependencies = async (directory, commands) => await (0, spinner_1.withSpinner)(`Install dependencies with ${commands.install}`, `Failed to install dependencies`, `Warnings while installing dependencies`, async () => {
if (process.env.GRAPH_CLI_TESTS) {
await gluegun_1.system.run(commands.link, { cwd: directory });
}
await gluegun_1.system.run(commands.install, { cwd: directory });
return true;
});
const runCodegen = async (directory, codegenCommand) => await (0, spinner_1.withSpinner)(`Generate ABI and schema types with ${codegenCommand}`, `Failed to generate code from ABI and GraphQL schema`, `Warnings while generating code from ABI and GraphQL schema`, async () => {
await gluegun_1.system.run(codegenCommand, { cwd: directory });
return true;
});
function printNextSteps({ subgraphName, directory }, { commands, }) {
const relativeDir = path_1.default.relative(process.cwd(), directory);
// Print instructions
this.log(`
Subgraph ${subgraphName} created in ${relativeDir}
`);
this.log(`Next steps:
1. Run \`graph auth\` to authenticate with your deploy key.
2. Type \`cd ${relativeDir}\` to enter the subgraph.
3. Run \`${commands.deploy}\` to deploy the subgraph.
Make sure to visit the documentation on https://thegraph.com/docs/ for further information.`);
}
async function initSubgraphFromExample({ fromExample, allowSimpleName, subgraphName, directory, skipInstall, skipGit, }, { commands, }) {
// Fail if the subgraph name is invalid
if (!revalidateSubgraphName.bind(this)(subgraphName, { allowSimpleName })) {
process.exitCode = 1;
return;
}
// Fail if the output directory already exists
if (gluegun_1.filesystem.exists(directory)) {
this.error(`Directory or file "${directory}" already exists`, { exit: 1 });
}
// Clone the example subgraph repository
const cloned = await (0, spinner_1.withSpinner)(`Cloning example subgraph`, `Failed to clone example subgraph`, `Warnings while cloning example subgraph`, async () => {
// Create a temporary directory
const prefix = path_1.default.join(os_1.default.tmpdir(), 'example-subgraph-');
const tmpDir = fs_1.default.mkdtempSync(prefix);
try {
await gluegun_1.system.run(`git clone https://github.com/graphprotocol/graph-tooling ${tmpDir}`);
// If an example is not specified, use the default one
if (fromExample === undefined || fromExample === true) {
fromExample = DEFAULT_EXAMPLE_SUBGRAPH;
}
// Legacy purposes when everything existed in examples repo
if (fromExample === 'ethereum/gravatar') {
fromExample = DEFAULT_EXAMPLE_SUBGRAPH;
}
const exampleSubgraphPath = path_1.default.join(tmpDir, 'examples', String(fromExample));
if (!gluegun_1.filesystem.exists(exampleSubgraphPath)) {
return { result: false, error: `Example not found: ${fromExample}` };
}
gluegun_1.filesystem.copy(exampleSubgraphPath, directory);
return true;
}
finally {
gluegun_1.filesystem.remove(tmpDir);
}
});
if (!cloned) {
this.exit(1);
return;
}
const networkConf = await (0, network_1.initNetworksConfig)(directory, 'address');
if (networkConf !== true) {
this.exit(1);
return;
}
// Update package.json to match the subgraph name
const prepared = await (0, spinner_1.withSpinner)(`Update subgraph name and commands in package.json`, `Failed to update subgraph name and commands in package.json`, `Warnings while updating subgraph name and commands in package.json`, async () => {
try {
// Load package.json
const pkgJsonFilename = gluegun_1.filesystem.path(directory, 'package.json');
const pkgJson = await gluegun_1.filesystem.read(pkgJsonFilename, 'json');
pkgJson.name = (0, subgraph_1.getSubgraphBasename)(subgraphName);
for (const name of Object.keys(pkgJson.scripts)) {
pkgJson.scripts[name] = pkgJson.scripts[name].replace('example', subgraphName);
}
delete pkgJson['license'];
delete pkgJson['repository'];
// Remove example's cli in favor of the local one (added via `npm link`)
if (process.env.GRAPH_CLI_TESTS) {
delete pkgJson['devDependencies']['@graphprotocol/graph-cli'];
}
// Write package.json
gluegun_1.filesystem.write(pkgJsonFilename, pkgJson, { jsonIndent: 2 });
return true;
}
catch (e) {
gluegun_1.filesystem.remove(directory);
this.error(`Failed to preconfigure the subgraph: ${e}`);
}
});
if (!prepared) {
this.exit(1);
return;
}
// Initialize a fresh Git repository
if (!skipGit) {
const repo = await initRepository(directory);
if (repo !== true) {
this.exit(1);
return;
}
}
// Install dependencies
if (!skipInstall) {
const installed = await installDependencies(directory, commands);
if (installed !== true) {
this.exit(1);
return;
}
}
// Run code-generation
const codegen = await runCodegen(directory, commands.codegen);
if (codegen !== true) {
this.exit(1);
return;
}
printNextSteps.bind(this)({ subgraphName, directory }, { commands });
}
async function initSubgraphFromContract({ protocolInstance, allowSimpleName, subgraphName, directory, abi, network, contract, indexEvents, contractName, node, startBlock, spkgPath, skipInstall, skipGit, }, { commands, addContract, }) {
const isSubstreams = protocolInstance.name === 'substreams';
// Fail if the subgraph name is invalid
if (!revalidateSubgraphName.bind(this)(subgraphName, { allowSimpleName })) {
this.exit(1);
return;
}
// Fail if the output directory already exists
if (gluegun_1.filesystem.exists(directory)) {
this.error(`Directory or file "${directory}" already exists`, { exit: 1 });
}
if (protocolInstance.hasABIs() &&
((0, schema_1.abiEvents)(abi).size === 0 ||
// @ts-expect-error TODO: the abiEvents result is expected to be a List, how's it an array?
(0, schema_1.abiEvents)(abi).length === 0)) {
// Fail if the ABI does not contain any events
this.error(`ABI does not contain any events`, { exit: 1 });
}
// Scaffold subgraph
const scaffold = await (0, spinner_1.withSpinner)(`Create subgraph scaffold`, `Failed to create subgraph scaffold`, `Warnings while creating subgraph scaffold`, async (spinner) => {
const scaffold = await (0, scaffold_1.generateScaffold)({
protocolInstance,
subgraphName,
abi,
network,
contract,
indexEvents,
contractName,
startBlock,
node,
spkgPath,
}, spinner);
await (0, scaffold_1.writeScaffold)(scaffold, directory, spinner);
return true;
});
if (scaffold !== true) {
process.exitCode = 1;
return;
}
if (protocolInstance.hasContract()) {
const identifierName = protocolInstance.getContract().identifierName();
const networkConf = await (0, network_1.initNetworksConfig)(directory, identifierName);
if (networkConf !== true) {
process.exitCode = 1;
return;
}
}
// Initialize a fresh Git repository
if (!skipGit) {
const repo = await initRepository(directory);
if (repo !== true) {
this.exit(1);
return;
}
}
if (!skipInstall) {
// Install dependencies
const installed = await installDependencies(directory, commands);
if (installed !== true) {
this.exit(1);
return;
}
}
// Substreams we have nothing to install or generate
if (!isSubstreams) {
// Run code-generation
const codegen = await runCodegen(directory, commands.codegen);
if (codegen !== true) {
this.exit(1);
return;
}
while (addContract) {
addContract = await addAnotherContract.bind(this)({
protocolInstance,
directory,
});
}
}
printNextSteps.bind(this)({ subgraphName, directory }, { commands });
}
async function addAnotherContract({ protocolInstance, directory, }) {
const addContractAnswer = await core_1.ux.prompt('Add another contract? (y/n)', {
required: true,
type: 'single',
});
const addContractConfirmation = addContractAnswer.toLowerCase() === 'y';
if (addContractConfirmation) {
const ProtocolContract = protocolInstance.getContract();
let contract = '';
for (;;) {
contract = await core_1.ux.prompt(`\nContract ${ProtocolContract.identifierName()}`, {
required: true,
});
const { valid, error } = (0, validation_1.validateContract)(contract, ProtocolContract);
if (valid) {
break;
}
this.log(`✖ ${error}`);
}
const contractName = await core_1.ux.prompt('\nContract Name', {
required: true,
default: 'Contract',
});
// Get the cwd before process.chdir in order to switch back in the end of command execution
const cwd = process.cwd();
try {
if (fs_1.default.existsSync(directory)) {
process.chdir(directory);
}
const commandLine = [contract, '--contract-name', contractName];
await add_1.default.run(commandLine);
}
catch (e) {
this.error(e);
}
finally {
// TODO: safer way of doing this?
process.chdir(cwd);
}
}
return addContractConfirmation;
}