diff --git a/.changeset/early-walls-talk.md b/.changeset/early-walls-talk.md new file mode 100644 index 000000000..8b47e8d9e --- /dev/null +++ b/.changeset/early-walls-talk.md @@ -0,0 +1,5 @@ +--- +'@graphprotocol/graph-cli': minor +--- + +`graph init` to allow downloading `spkg.io` files diff --git a/.changeset/funny-plums-pump.md b/.changeset/funny-plums-pump.md new file mode 100644 index 000000000..d694aa6ac --- /dev/null +++ b/.changeset/funny-plums-pump.md @@ -0,0 +1,5 @@ +--- +'@graphprotocol/graph-cli': patch +--- + +Add support to download spkg files from spkg.io diff --git a/packages/cli/src/command-helpers/download.ts b/packages/cli/src/command-helpers/download.ts new file mode 100644 index 000000000..36feeab10 --- /dev/null +++ b/packages/cli/src/command-helpers/download.ts @@ -0,0 +1,24 @@ +import { createWriteStream } from 'fs'; +import fetchWrapper from '../fetch'; + +export async function downloadFile(fileUrl: string, outputLocationPath: string) { + const writer = createWriteStream(outputLocationPath); + const stream = new WritableStream({ + write(chunk) { + writer.write(chunk); + }, + }); + const url = fileUrl.startsWith('https://') ? fileUrl : `https://${fileUrl}`; + return fetchWrapper(url, { + method: 'GET', + }) + .then(response => { + if (!response.body) { + return Promise.reject('No file found'); + } + return response.body.pipeTo(stream); + }) + .then(() => { + return Promise.resolve(outputLocationPath); + }); +} diff --git a/packages/cli/src/command-helpers/spkg.test.ts b/packages/cli/src/command-helpers/spkg.test.ts new file mode 100644 index 000000000..a4528d232 --- /dev/null +++ b/packages/cli/src/command-helpers/spkg.test.ts @@ -0,0 +1,47 @@ +import path from 'path'; +import { describe, expect, it } from 'vitest'; +import { getSpkgFilePath, isSpkgUrl } from '../../src/command-helpers/spkg'; + +describe('getSpkgFilePath', () => { + it('should return the correct file path', () => { + const spkgUrl = 'https://example.com/package.spkg'; + const directory = '/home/testuser/development/testgraph'; + const expectedFilePath = path.join(directory, 'package.spkg'); + + const filePath = getSpkgFilePath(spkgUrl, directory); + + expect(filePath).to.equal(expectedFilePath); + }); + + it('should throw an error for invalid spkg url', () => { + const spkgUrl = ''; + const directory = '/home/joshua/development/graphprotocol/graph-tooling/packages/cli'; + + expect(() => getSpkgFilePath(spkgUrl, directory)).to.throw('Invalid spkg url'); + }); +}); + +describe('isSpkgUrl', () => { + it('should return true for valid spkg url with https', () => { + const spkgUrl = 'https://spkg.io/streamingfast/package.spkg'; + const result = isSpkgUrl(spkgUrl); + expect(result).toBe(true); + }); + + it('should return true for valid spkg url', () => { + const spkgUrl = 'spkg.io/streamingfast/package.spkg'; + const result = isSpkgUrl(spkgUrl); + expect(result).toBe(true); + }); + + it('should return false for invalid spkg url', () => { + const spkgUrl = 'https://example.com/package.spkg'; + const result = isSpkgUrl(spkgUrl); + expect(result).toBe(false); + }); + it('should return false for non-url string', () => { + const spkgUrl = 'streamingfast/package.spkg'; + const result = isSpkgUrl(spkgUrl); + expect(result).toBe(false); + }); +}); diff --git a/packages/cli/src/command-helpers/spkg.ts b/packages/cli/src/command-helpers/spkg.ts new file mode 100644 index 000000000..a23f0e0a9 --- /dev/null +++ b/packages/cli/src/command-helpers/spkg.ts @@ -0,0 +1,18 @@ +// validator that checks if spkg exists or is valid url + +import path from 'path'; +import { filesystem } from 'gluegun'; + +export const isSpkgUrl = (value: string) => { + return value.startsWith(`https://spkg.io`) || value.startsWith(`spkg.io`); +}; + +export const validateSpkg = (value: string) => { + return filesystem.exists(value) || isSpkgUrl(value); +}; + +export const getSpkgFilePath = (spkgUrl: string, directory: string) => { + const spkgFileName = spkgUrl.split('/').pop(); + if (!spkgFileName) throw new Error('Invalid spkg url'); + return path.join(directory, spkgFileName); +}; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5625eeb50..efbe2ab63 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -9,11 +9,13 @@ import { loadAbiFromEtherscan, loadStartBlockForContract, } from '../command-helpers/abi'; +import { downloadFile } from '../command-helpers/download'; import { initNetworksConfig } from '../command-helpers/network'; import { chooseNodeUrl, SUBGRAPH_STUDIO_URL } from '../command-helpers/node'; import { generateScaffold, writeScaffold } from '../command-helpers/scaffold'; import { sortWithPriority } from '../command-helpers/sort'; import { withSpinner } from '../command-helpers/spinner'; +import { getSpkgFilePath, isSpkgUrl, validateSpkg } from '../command-helpers/spkg'; import { getSubgraphBasename, validateSubgraphName } from '../command-helpers/subgraph'; import { GRAPH_CLI_SHARED_HEADERS } from '../constants'; import debugFactory from '../debug'; @@ -726,11 +728,10 @@ async function processInitForm( { type: 'input', name: 'spkg', - message: 'SPKG file (path)', + message: 'SPKG file (path or spkg.io url)', initial: () => initSpkgPath, skip: () => !isSubstreams || !!initSpkgPath, - validate: value => - filesystem.exists(initSpkgPath || value) ? true : 'SPKG file does not exist', + validate: value => validateSpkg(value) || 'Invalid SPKG file', }, ]); @@ -1206,6 +1207,16 @@ async function initSubgraphFromContract( this.error(`ABI does not contain any events`, { exit: 1 }); } + let spkgFilePath = spkgPath; + // if url let's overwrite with the file path + if (spkgPath && isSpkgUrl(spkgPath)) { + try { + spkgFilePath = await getSpkgFilePath(spkgPath, directory); + } catch (e) { + this.error(e.message, { exit: 1 }); + } + } + // Scaffold subgraph const scaffold = await withSpinner( `Create subgraph scaffold`, @@ -1223,11 +1234,14 @@ async function initSubgraphFromContract( contractName, startBlock, node, - spkgPath, + spkgPath: spkgFilePath, }, spinner, ); await writeScaffold(scaffold, directory, spinner); + if (spkgPath && spkgFilePath && isSpkgUrl(spkgPath)) { + await downloadFile(spkgPath, spkgFilePath); + } return true; }, );