@ -1,46 +1,57 @@
import { confirm } from '@inquirer/prompts' ;
import input from '@inquirer/input' ;
import { Separator , checkbox , confirm } from '@inquirer/prompts' ;
import select from '@inquirer/select' ;
import { ethers } from 'ethers' ;
import { ethers } from 'ethers' ;
import { timeout } from '@hyperlane-xyz/utils' ;
import { ChainName } from '@hyperlane-xyz/sdk' ;
import { ProtocolType , timeout } from '@hyperlane-xyz/utils' ;
import { Contexts } from '../../config/contexts.js' ;
import { getChain } from '../../config/registry.js' ;
import { getEnvironmentConfig } from '../../scripts/core-utils.js' ;
import {
import {
RelayerHelmManager ,
ScraperHelmManager ,
ValidatorHelmManager ,
getSecretRpcEndpoints ,
getSecretRpcEndpoints ,
getSecretRpcEndpointsLatestVersionName ,
getSecretRpcEndpointsLatestVersionName ,
secretRpcEndpointsExist ,
secretRpcEndpointsExist ,
setSecretRpcEndpoints ,
setSecretRpcEndpoints ,
} from '../agents/index.js' ;
} from '../agents/index.js' ;
import { DeployEnvironment } from '../config/environment.js' ;
import { KeyFunderHelmManager } from '../funding/key-funder.js' ;
import { KathyHelmManager } from '../helloworld/kathy.js' ;
import { disableGCPSecretVersion } from './gcloud.js' ;
import { disableGCPSecretVersion } from './gcloud.js' ;
import { isEthereumProtocolChain } from './utils.js' ;
import { HelmManager } from './helm.js' ;
import { K8sResourceType , refreshK8sResources } from './k8s.js' ;
export async function testProviders ( rpcUrlsArray : string [ ] ) : Promise < boolean > {
/ * *
let providersSucceeded = true ;
* Set the RPC URLs for the given chain in the given environment interactively .
for ( const url of rpcUrlsArray ) {
* Includes an interactive experience for selecting new RPC URLs , confirming the change ,
const provider = new ethers . providers . StaticJsonRpcProvider ( url ) ;
* updating the secret , and refreshing dependent k8s resources .
try {
* @param environment The environment to set the RPC URLs in
const blockNumber = await timeout ( provider . getBlockNumber ( ) , 5000 ) ;
* @param chain The chain to set the RPC URLs for
console . log ( ` Valid provider for ${ url } with block number ${ blockNumber } ` ) ;
* /
} catch ( e ) {
export async function setRpcUrlsInteractive (
console . error ( ` Provider failed: ${ url } ` ) ;
providersSucceeded = false ;
}
}
return providersSucceeded ;
}
export async function setAndVerifyRpcUrls (
environment : string ,
environment : string ,
chain : string ,
chain : string ,
rpcUrlsArray : string [ ] ,
) : Promise < void > {
) : Promise < void > {
const secretPayload = JSON . stringify ( rpcUrlsArray ) ;
try {
try {
await displayCurrentSecrets ( environment , chain ) ;
const currentSecrets = await getAndDisplayCurrentSecrets (
await confirmSetSecrets ( environment , chain , secretPayload ) ;
environment ,
await testProvidersIfNeeded ( chain , rpcUrlsArray ) ;
chain ,
) ;
const newRpcUrls = await inputRpcUrls ( chain , currentSecrets ) ;
console . log ( ` Selected RPC URLs: ${ formatRpcUrls ( newRpcUrls ) } \ n ` ) ;
const secretPayload = JSON . stringify ( newRpcUrls ) ;
await confirmSetSecretsInteractive ( environment , chain , secretPayload ) ;
await updateSecretAndDisablePrevious ( environment , chain , secretPayload ) ;
await updateSecretAndDisablePrevious ( environment , chain , secretPayload ) ;
await refreshDependentK8sResourcesInteractive (
environment as DeployEnvironment ,
chain ,
) ;
} catch ( error : any ) {
} catch ( error : any ) {
console . error (
console . error (
` Error occurred while setting RPC URLs for ${ chain } : ` ,
` Error occurred while setting RPC URLs for ${ chain } : ` ,
@ -50,28 +61,167 @@ export async function setAndVerifyRpcUrls(
}
}
}
}
async function displayCurrentSecrets (
async function getAn dD isplayCurrentSecrets(
environment : string ,
environment : string ,
chain : string ,
chain : string ,
) : Promise < void > {
) : Promise < string [ ] > {
const secretExists = await secretRpcEndpointsExist ( environment , chain ) ;
const secretExists = await secretRpcEndpointsExist ( environment , chain ) ;
if ( ! secretExists ) {
if ( ! secretExists ) {
console . log (
console . log (
` No secret rpc urls found for ${ chain } in ${ environment } environment \ n ` ,
` No secret rpc urls found for ${ chain } in ${ environment } environment \ n ` ,
) ;
) ;
} else {
return [ ] ;
const currentSecrets = await getSecretRpcEndpoints ( environment , chain ) ;
}
console . log (
` Current secrets found for ${ chain } in ${ environment } environment: \ n ${ JSON . stringify (
const currentSecrets = await getSecretRpcEndpoints ( environment , chain ) ;
currentSecrets ,
console . log (
null ,
` Current secrets found for ${ chain } in ${ environment } environment: \ n ${ formatRpcUrls (
2 ,
currentSecrets ,
) } \ n ` ,
) } \ n ` ,
) ;
) ;
return currentSecrets ;
}
// Copied from @inquirer/prompts as it's not exported :(
type Choice < Value > = {
value : Value ;
name? : string ;
description? : string ;
short? : string ;
disabled? : boolean | string ;
type ? : never ;
} ;
/ * *
* Prompt the user to input RPC URLs for the given chain .
* The user can choose to input new URLs , use all registry URLs , or use existing URLs
* from secrets or the registry .
* @param chain The chain to input RPC URLs for
* @param existingUrls The existing RPC URLs for the chain
* @returns The selected RPC URLs
* /
async function inputRpcUrls (
chain : string ,
existingUrls : string [ ] ,
) : Promise < string [ ] > {
const selectedUrls : string [ ] = [ ] ;
const registryUrls = getChain ( chain ) . rpcUrls . map ( ( rpc ) = > rpc . http ) ;
const existingUrlChoices : Array < Choice < string > > = existingUrls . map (
( url , i ) = > {
return {
value : url ,
name : ` ${ url } (existing index ${ i } ) ` ,
} ;
} ,
) ;
const registryUrlChoices : Array < Choice < string > > = registryUrls . map (
( url , i ) = > {
return {
value : url ,
name : ` [PUBLIC] ${ url } (registry index ${ i } ) ` ,
} ;
} ,
) ;
enum SystemChoice {
ADD_NEW = 'Add new RPC URL' ,
DONE = 'Done' ,
USE_REGISTRY_URLS = 'Use all registry URLs' ,
REMOVE_LAST = 'Remove last RPC URL' ,
}
}
const pushSelectedUrl = async ( newUrl : string ) = > {
const providerHealthy = await testProvider ( chain , newUrl ) ;
if ( ! providerHealthy ) {
const yes = await confirm ( {
message : ` Provider at ${ newUrl } is not healthy. Do you want to continue adding it? \ n ` ,
} ) ;
if ( ! yes ) {
console . log ( 'Skipping provider' ) ;
return ;
}
}
selectedUrls . push ( newUrl ) ;
} ;
const separator = new Separator ( '-----' ) ;
while ( true ) {
console . log ( ` Selected RPC URLs: ${ formatRpcUrls ( selectedUrls ) } \ n ` ) ;
// Sadly @inquirer/prompts doesn't expose the types needed here
const choices : ( Separator | Choice < any > ) [ ] = [
. . . [ SystemChoice . DONE , SystemChoice . ADD_NEW ] . map ( ( choice ) = > ( {
value : choice ,
} ) ) ,
{
value : SystemChoice.USE_REGISTRY_URLS ,
name : ` Use registry URLs ( ${ JSON . stringify ( registryUrls ) } ) ` ,
} ,
separator ,
. . . existingUrlChoices ,
separator ,
. . . registryUrlChoices ,
] ;
if ( selectedUrls . length > 0 ) {
choices . push ( separator ) ;
choices . push ( {
value : SystemChoice.REMOVE_LAST ,
} ) ;
}
const selection = await select ( {
message : 'Select RPC URL' ,
choices ,
pageSize : 30 ,
} ) ;
if ( selection === SystemChoice . DONE ) {
console . log ( 'Done selecting RPC URLs' ) ;
break ;
} else if ( selection === SystemChoice . ADD_NEW ) {
const newUrl = await input ( {
message : 'Enter new RPC URL' ,
} ) ;
await pushSelectedUrl ( newUrl ) ;
} else if ( selection === SystemChoice . REMOVE_LAST ) {
selectedUrls . pop ( ) ;
} else if ( selection === SystemChoice . USE_REGISTRY_URLS ) {
for ( const url of registryUrls ) {
await pushSelectedUrl ( url ) ;
}
console . log ( 'Added all registry URLs' ) ;
break ;
} else {
// If none of the above, a URL was chosen
let index = existingUrlChoices . findIndex (
( choice ) = > choice . value === selection ,
) ;
if ( index !== - 1 ) {
existingUrlChoices . splice ( index , 1 ) ;
}
index = registryUrlChoices . findIndex (
( choice ) = > choice . value === selection ,
) ;
if ( index !== - 1 ) {
registryUrlChoices . splice ( index , 1 ) ;
}
await pushSelectedUrl ( selection ) ;
}
console . log ( '========' ) ;
}
return selectedUrls ;
}
}
async function confirmSetSecrets (
/ * *
* A prompt to confirm setting the given secret payload for the given chain in the given environment .
* /
async function confirmSetSecretsInteractive (
environment : string ,
environment : string ,
chain : string ,
chain : string ,
secretPayload : string ,
secretPayload : string ,
@ -86,33 +236,13 @@ async function confirmSetSecrets(
}
}
}
}
async function testProvidersIfNeeded (
/ * *
chain : string ,
* Non - interactively updates the secret for the given chain in the given environment with the given payload .
rpcUrlsArray : string [ ] ,
* Disables the previous version of the secret if it exists .
) : Promise < void > {
* @param environment The environment to update the secret in
if ( isEthereumProtocolChain ( chain ) ) {
* @param chain The chain to update the secret for
console . log ( '\nTesting providers...' ) ;
* @param secretPayload The new secret payload to set
const testPassed = await testProviders ( rpcUrlsArray ) ;
* /
if ( ! testPassed ) {
console . error ( 'At least one provider failed.' ) ;
throw new Error ( 'Provider test failed' ) ;
}
const confirmedProviders = await confirm ( {
message : ` All providers passed. Do you want to continue setting the secret? \ n ` ,
} ) ;
if ( ! confirmedProviders ) {
console . log ( 'Continuing without setting secret.' ) ;
throw new Error ( 'User cancelled operation after provider test' ) ;
}
} else {
console . log (
'Skipping provider testing as chain is not an Ethereum protocol chain.' ,
) ;
}
}
async function updateSecretAndDisablePrevious (
async function updateSecretAndDisablePrevious (
environment : string ,
environment : string ,
chain : string ,
chain : string ,
@ -139,3 +269,129 @@ async function updateSecretAndDisablePrevious(
}
}
}
}
}
}
/ * *
* Interactively refreshes dependent k8s resources for the given chain in the given environment .
* Allows for helm releases to be selected for refreshing . Refreshing involves first deleting
* secrets , expecting them to be recreated by external - secrets , and then deleting pods to restart
* them with the new secrets .
* @param environment The environment to refresh resources in
* @param chain The chain to refresh resources for
* /
async function refreshDependentK8sResourcesInteractive (
environment : DeployEnvironment ,
chain : string ,
) : Promise < void > {
const cont = await confirm ( {
message : ` Do you want to refresh dependent k8s resources for ${ chain } in ${ environment } ? ` ,
} ) ;
if ( ! cont ) {
console . log ( 'Skipping refresh of k8s resources' ) ;
return ;
}
const envConfig = getEnvironmentConfig ( environment ) ;
const contextHelmManagers : [ string , HelmManager < any > ] [ ] = [ ] ;
const pushContextHelmManager = (
context : string ,
manager : HelmManager < any > ,
) = > {
contextHelmManagers . push ( [ context , manager ] ) ;
} ;
for ( const [ context , agentConfig ] of Object . entries ( envConfig . agents ) ) {
if ( agentConfig . relayer ) {
pushContextHelmManager ( context , new RelayerHelmManager ( agentConfig ) ) ;
}
if ( agentConfig . validators ) {
pushContextHelmManager (
context ,
new ValidatorHelmManager ( agentConfig , chain ) ,
) ;
}
if ( agentConfig . scraper ) {
pushContextHelmManager ( context , new ScraperHelmManager ( agentConfig ) ) ;
}
if ( context == Contexts . Hyperlane ) {
// Key funder
pushContextHelmManager (
context ,
KeyFunderHelmManager . forEnvironment ( environment ) ,
) ;
// Kathy - only expected to be running as a long-running service in the
// Hyperlane context
if ( envConfig . helloWorld ? . hyperlane ? . addresses [ chain ] ) {
pushContextHelmManager (
context ,
KathyHelmManager . forEnvironment ( environment , context ) ,
) ;
}
}
}
const selection = await checkbox ( {
message :
'Select deployments to refresh (update secrets & restart any pods)' ,
choices : contextHelmManagers.map ( ( [ context , helmManager ] , i ) = > ( {
name : ` ${ helmManager . helmReleaseName } (context: ${ context } ) ` ,
value : i ,
// By default, all deployments are selected
checked : true ,
} ) ) ,
} ) ;
const selectedHelmManagers = contextHelmManagers
. map ( ( [ _ , m ] ) = > m )
. filter ( ( _ , m ) = > selection . includes ( m ) ) ;
await refreshK8sResources (
selectedHelmManagers ,
K8sResourceType . SECRET ,
environment ,
) ;
await refreshK8sResources (
selectedHelmManagers ,
K8sResourceType . POD ,
environment ,
) ;
}
/ * *
* Test the provider at the given URL , returning false if the provider is unhealthy
* or related to a different chain . No - op for non - Ethereum chains .
* @param chain The chain to test the provider for
* @param url The URL of the provider
* /
async function testProvider ( chain : ChainName , url : string ) : Promise < boolean > {
const chainMetadata = getChain ( chain ) ;
if ( chainMetadata . protocol !== ProtocolType . Ethereum ) {
console . log ( ` Skipping provider test for non-Ethereum chain ${ chain } ` ) ;
return true ;
}
const provider = new ethers . providers . StaticJsonRpcProvider ( url ) ;
const expectedChainId = chainMetadata . chainId ;
try {
const [ blockNumber , providerNetwork ] = await timeout (
Promise . all ( [ provider . getBlockNumber ( ) , provider . getNetwork ( ) ] ) ,
5000 ,
) ;
if ( providerNetwork . chainId !== expectedChainId ) {
throw new Error (
` Expected chainId ${ expectedChainId } , got ${ providerNetwork . chainId } ` ,
) ;
}
console . log (
` ✅ Valid provider for ${ url } with block number ${ blockNumber } ` ,
) ;
return true ;
} catch ( e ) {
console . error ( ` Provider failed: ${ url } \ nError: ${ e } ` ) ;
return false ;
}
}
function formatRpcUrls ( rpcUrls : string [ ] ) : string {
return JSON . stringify ( rpcUrls , null , 2 ) ;
}