Skip to content

Commit ea8c867

Browse files
committed
refactor prompts
1 parent 5f071bc commit ea8c867

File tree

5 files changed

+154
-114
lines changed

5 files changed

+154
-114
lines changed

packages/cli/src/command-helpers/contracts.ts

Lines changed: 60 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { NetworksRegistry } from '@pinax/graph-networks-registry';
33
import debugFactory from '../debug.js';
44
import fetch from '../fetch.js';
55
import ABI from '../protocols/ethereum/abi.js';
6-
import { withSpinner } from './spinner.js';
76

87
const logger = debugFactory('graph-cli:contract-service');
98

@@ -72,108 +71,84 @@ export class ContractService {
7271

7372
async getABI(ABICtor: typeof ABI, networkId: string, address: string) {
7473
const urls = this.getEtherscanUrls(networkId);
75-
7674
let errors: string[] = [];
77-
return await withSpinner(
78-
`Fetching ABI from contract API`,
79-
`Failed to fetch ABI from contract API`,
80-
`Warnings while fetching ABI from contract API`,
81-
async () => {
82-
if (!urls.length) {
83-
throw new Error(`No contract API available for ${networkId} in the registry`);
84-
}
85-
for (const url of urls) {
86-
try {
87-
const json = await this.fetchFromEtherscan(
88-
`${url}?module=contract&action=getabi&address=${address}`,
89-
);
90-
91-
if (json) {
92-
return new ABICtor('Contract', undefined, immutable.fromJS(JSON.parse(json.result)));
93-
}
94-
throw new Error(`no result: ${JSON.stringify(json)}`);
95-
} catch (error) {
96-
logger(`Failed to fetch from ${url}: ${error}`);
97-
errors.push(`${error}`);
98-
}
75+
if (!urls.length) {
76+
throw new Error(`No contract API available for ${networkId} in the registry`);
77+
}
78+
for (const url of urls) {
79+
try {
80+
const json = await this.fetchFromEtherscan(
81+
`${url}?module=contract&action=getabi&address=${address}`,
82+
);
83+
84+
if (json) {
85+
return new ABICtor('Contract', undefined, immutable.fromJS(JSON.parse(json.result)));
9986
}
87+
throw new Error(`no result: ${JSON.stringify(json)}`);
88+
} catch (error) {
89+
logger(`Failed to fetch from ${url}: ${error}`);
90+
errors.push(`${error}`);
91+
}
92+
}
10093

101-
throw new Error(errors?.[0]);
102-
},
103-
);
94+
throw new Error(errors?.[0]);
10495
}
10596

10697
async getStartBlock(networkId: string, address: string): Promise<string> {
10798
const urls = this.getEtherscanUrls(networkId);
99+
if (!urls.length) {
100+
throw new Error(`No contract API available for ${networkId} in the registry`);
101+
}
102+
for (const url of urls) {
103+
try {
104+
const json = await this.fetchFromEtherscan(
105+
`${url}?module=contract&action=getcontractcreation&contractaddresses=${address}`,
106+
);
108107

109-
return await withSpinner(
110-
`Fetching start block`,
111-
`Failed to fetch start block`,
112-
`Warnings while fetching deploy contract transaction from contract API`,
113-
async () => {
114-
if (!urls.length) {
115-
throw new Error(`No contract API available for ${networkId} in the registry`);
116-
}
117-
for (const url of urls) {
118-
try {
119-
const json = await this.fetchFromEtherscan(
120-
`${url}?module=contract&action=getcontractcreation&contractaddresses=${address}`,
121-
);
122-
123-
if (json?.result?.length) {
124-
if (json.result[0]?.blockNumber) {
125-
return json.result[0].blockNumber;
126-
}
127-
const txHash = json.result[0].txHash;
128-
const tx = await this.fetchTransactionByHash(networkId, txHash);
129-
if (!tx?.blockNumber) {
130-
throw new Error(`no blockNumber: ${JSON.stringify(tx)}`);
131-
}
132-
return Number(tx.blockNumber).toString();
133-
}
134-
throw new Error(`no result: ${JSON.stringify(json)}`);
135-
} catch (error) {
136-
logger(`Failed to fetch start block from ${url}: ${error}`);
108+
if (json?.result?.length) {
109+
if (json.result[0]?.blockNumber) {
110+
return json.result[0].blockNumber;
111+
}
112+
const txHash = json.result[0].txHash;
113+
const tx = await this.fetchTransactionByHash(networkId, txHash);
114+
if (!tx?.blockNumber) {
115+
throw new Error(`no blockNumber: ${JSON.stringify(tx)}`);
137116
}
117+
return Number(tx.blockNumber).toString();
138118
}
119+
throw new Error(`no result: ${JSON.stringify(json)}`);
120+
} catch (error) {
121+
logger(`Failed to fetch start block from ${url}: ${error}`);
122+
}
123+
}
139124

140-
throw new Error(`Failed to fetch deploy contract transaction for ${address}`);
141-
},
142-
);
125+
throw new Error(`Failed to fetch deploy contract transaction for ${address}`);
143126
}
144127

145128
async getContractName(networkId: string, address: string): Promise<string> {
146129
const urls = this.getEtherscanUrls(networkId);
147-
148-
return await withSpinner(
149-
`Fetching contract name`,
150-
`Failed to fetch contract name`,
151-
`Warnings while fetching contract name from contract API`,
152-
async () => {
153-
if (!urls.length) {
154-
throw new Error(`No contract API available for ${networkId} in the registry`);
155-
}
156-
for (const url of urls) {
157-
try {
158-
const json = await this.fetchFromEtherscan(
159-
`${url}?module=contract&action=getsourcecode&address=${address}`,
160-
);
161-
162-
if (json) {
163-
const { ContractName } = json.result[0];
164-
if (ContractName !== '') {
165-
return ContractName;
166-
}
167-
}
168-
throw new Error(`no result: ${JSON.stringify(json)}`);
169-
} catch (error) {
170-
logger(`Failed to fetch from ${url}: ${error}`);
130+
if (!urls.length) {
131+
throw new Error(`No contract API available for ${networkId} in the registry`);
132+
}
133+
for (const url of urls) {
134+
try {
135+
const json = await this.fetchFromEtherscan(
136+
`${url}?module=contract&action=getsourcecode&address=${address}`,
137+
);
138+
139+
if (json) {
140+
const { ContractName } = json.result[0];
141+
if (ContractName !== '') {
142+
return ContractName;
171143
}
172144
}
145+
throw new Error(`no result: ${JSON.stringify(json)}`);
146+
} catch (error) {
147+
logger(`Failed to fetch from ${url}: ${error}`);
148+
}
149+
}
173150

174-
throw new Error(`Failed to fetch contract name for ${address}`);
175-
},
176-
);
151+
throw new Error(`Failed to fetch contract name for ${address}`);
177152
}
178153

179154
private async fetchTransactionByHash(networkId: string, txHash: string) {

packages/cli/src/command-helpers/prompt-manager.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,34 @@ export class PromptManager {
55
private steps: PromptOptions[] = [];
66
private currentStep = 0;
77
private results: any[] = [];
8+
private stepLines: number[] = [];
89

10+
// adds a step prompt
911
addStep(options: PromptOptions) {
1012
this.steps.push(options);
13+
this.stepLines.push(1);
14+
}
15+
16+
// "options" doesn't take closure, so to populate it dynamically we need to call this
17+
setOptions(stepName: string, options: Partial<PromptOptions>) {
18+
const step = this.steps.find(s => s.name === stepName);
19+
if (step) {
20+
Object.assign(step, options);
21+
}
22+
}
23+
24+
// can be called externally if more than one line is added on the step
25+
addLine() {
26+
this.stepLines[this.currentStep]++;
27+
}
28+
29+
// clears the lines added during the step
30+
private clearStepLines() {
31+
const linesToClear = 1 + this.stepLines[this.currentStep];
32+
for (let i = 0; i < linesToClear; i++) {
33+
process.stdout.write('\x1b[1A\x1b[2K'); // Move up and clear line
34+
}
35+
this.stepLines[this.currentStep] = 1;
1136
}
1237

1338
// runs all steps and returns the results
@@ -36,8 +61,7 @@ export class PromptManager {
3661
process.stdout.write('\n');
3762
process.exit(0);
3863
}
39-
// delete 2 lines
40-
process.stdout.write('\x1b[1A\x1b[2K\x1b[1A\x1b[2K');
64+
4165
this.currentStep--;
4266
while (this.currentStep > 0) {
4367
delete this.results[this.currentStep];
@@ -46,6 +70,7 @@ export class PromptManager {
4670
if (!shouldSkip) break;
4771
this.currentStep--;
4872
}
73+
this.clearStepLines();
4974
}
5075
}
5176

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { prompt } from 'gluegun';
2+
3+
export async function retryWithPrompt<T>(func: () => Promise<T>): Promise<T | undefined> {
4+
for (;;) {
5+
try {
6+
return await func();
7+
} catch (_) {
8+
try {
9+
const { retry } = await prompt.ask({
10+
type: 'confirm',
11+
name: 'retry',
12+
message: 'Do you want to retry?',
13+
initial: true,
14+
});
15+
16+
if (!retry) {
17+
break;
18+
}
19+
} catch (_) {
20+
break;
21+
}
22+
}
23+
}
24+
return undefined;
25+
}

packages/cli/src/commands/add.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NetworksRegistry } from '@pinax/graph-networks-registry';
55
import { ContractService } from '../command-helpers/contracts.js';
66
import * as DataSourcesExtractor from '../command-helpers/data-sources.js';
77
import { updateNetworksFile } from '../command-helpers/network.js';
8+
import { retryWithPrompt } from '../command-helpers/retry.js';
89
import {
910
generateDataSource,
1011
writeABI,
@@ -100,7 +101,14 @@ export default class AddCommand extends Command {
100101
try {
101102
if (isLocalHost) throw Error; // Triggers user prompting without waiting for Etherscan lookup to fail
102103

103-
ethabi = await contractService?.getABI(EthereumABI, network, address);
104+
ethabi = await retryWithPrompt(() =>
105+
withSpinner(
106+
'Fetching ABI from contract API...',
107+
'Failed to fetch ABI',
108+
'Warning fetching ABI',
109+
() => contractService?.getABI(EthereumABI, network, address),
110+
),
111+
);
104112
} catch (error) {
105113
// we cannot ask user to do prompt in test environment
106114
if (process.env.NODE_ENV !== 'test') {

packages/cli/src/commands/init.ts

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DEFAULT_IPFS_URL } from '../command-helpers/ipfs.js';
1111
import { initNetworksConfig } from '../command-helpers/network.js';
1212
import { chooseNodeUrl } from '../command-helpers/node.js';
1313
import { PromptManager } from '../command-helpers/prompt-manager.js';
14+
import { retryWithPrompt } from '../command-helpers/retry.js';
1415
import { generateScaffold, writeScaffold } from '../command-helpers/scaffold.js';
1516
import { sortWithPriority } from '../command-helpers/sort.js';
1617
import { withSpinner } from '../command-helpers/spinner.js';
@@ -361,26 +362,6 @@ async function processFromExampleInitForm(
361362
}
362363
}
363364

364-
async function retryWithPrompt<T>(func: () => Promise<T>): Promise<T | undefined> {
365-
for (;;) {
366-
try {
367-
return await func();
368-
} catch (_) {
369-
const { retry } = await prompt.ask({
370-
type: 'confirm',
371-
name: 'retry',
372-
message: 'Do you want to retry?',
373-
initial: true,
374-
});
375-
376-
if (!retry) {
377-
break;
378-
}
379-
}
380-
}
381-
return undefined;
382-
}
383-
384365
async function processInitForm(
385366
this: InitCommand,
386367
{
@@ -493,6 +474,18 @@ async function processInitForm(
493474
result: value => {
494475
initDebugger.extend('processInitForm')('networkId: %O', value);
495476
network = networks.find(n => n.id === value)!;
477+
promptManager.setOptions('protocol', {
478+
choices: [
479+
{
480+
message: 'Smart contract',
481+
name: network.graphNode?.protocol ?? '',
482+
value: 'contract',
483+
},
484+
{ message: 'Substreams', name: 'substreams', value: 'substreams' },
485+
{ message: 'Subgraph', name: 'subgraph', value: 'subgraph' },
486+
].filter(({ name }) => name),
487+
});
488+
496489
return value;
497490
},
498491
});
@@ -502,16 +495,15 @@ async function processInitForm(
502495
name: 'protocol',
503496
message: 'Source',
504497
choices: [
505-
{ message: 'Smart contract', name: network.graphNode?.protocol ?? '', value: 'contract' },
506498
{ message: 'Substreams', name: 'substreams', value: 'substreams' },
507499
{ message: 'Subgraph', name: 'subgraph', value: 'subgraph' },
508500
].filter(({ name }) => name),
509501
validate: name => {
510502
if (name === 'arweave') {
511-
return 'Arweave only supported via substreams';
503+
return 'Arweave are only supported via substreams';
512504
}
513505
if (name === 'cosmos') {
514-
return 'Cosmos chains only supported via substreams';
506+
return 'Cosmos chains are only supported via substreams';
515507
}
516508
return true;
517509
},
@@ -587,22 +579,37 @@ async function processInitForm(
587579
// If ABI is not provided, try to fetch it from Etherscan API
588580
if (protocolInstance.hasABIs() && !initAbi) {
589581
abiFromApi = await retryWithPrompt(() =>
590-
contractService.getABI(protocolInstance.getABI(), network.id, address),
582+
withSpinner(
583+
'Fetching ABI from contract API...',
584+
'Failed to fetch ABI',
585+
'Warning fetching ABI',
586+
() => contractService.getABI(protocolInstance.getABI(), network.id, address),
587+
),
591588
);
592589
initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
593590
}
594591
// If startBlock is not provided, try to fetch it from Etherscan API
595592
if (!initStartBlock) {
596593
startBlock = await retryWithPrompt(() =>
597-
contractService.getStartBlock(network.id, address),
594+
withSpinner(
595+
'Fetching start block from contract API...',
596+
'Failed to fetch start block',
597+
'Warning fetching start block',
598+
() => contractService.getStartBlock(network.id, address),
599+
),
598600
);
599601
initDebugger.extend('processInitForm')("startBlockFromEtherscan: '%s'", startBlock);
600602
}
601603

602604
// If contract name is not provided, try to fetch it from Etherscan API
603605
if (!initContractName) {
604606
contractName = await retryWithPrompt(() =>
605-
contractService.getContractName(network.id, address),
607+
withSpinner(
608+
'Fetching contract name from contract API...',
609+
'Failed to fetch contract name',
610+
'Warning fetching contract name',
611+
() => contractService.getContractName(network.id, address),
612+
),
606613
);
607614
initDebugger.extend('processInitForm')("contractNameFromEtherscan: '%s'", contractName);
608615
}

0 commit comments

Comments
 (0)