Change relayer funder to key funder, allowing multiple roles to be funded (#876)

* wip

* It's working

* All put together

* renames

* Ensure --contexts-to-fund works

* Move to roles per context

* add example usage

* rm dry run

* Update typescript/infra/scripts/funding/fund-keys-from-deployer.ts

Co-authored-by: Mattie Conover <git@mconover.dev>

* Update typescript/infra/scripts/funding/fund-keys-from-deployer.ts

Co-authored-by: Mattie Conover <git@mconover.dev>

* PR comments

Co-authored-by: Mattie Conover <git@mconover.dev>
pull/883/head
Trevor Porter 2 years ago committed by GitHub
parent 93280059d6
commit 51b594b248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      typescript/infra/config/environments/mainnet/funding.ts
  2. 4
      typescript/infra/config/environments/mainnet/index.ts
  3. 9
      typescript/infra/config/environments/testnet2/funding.ts
  4. 4
      typescript/infra/config/environments/testnet2/index.ts
  5. 2
      typescript/infra/helm/key-funder/Chart.yaml
  6. 0
      typescript/infra/helm/key-funder/templates/_helpers.tpl
  7. 14
      typescript/infra/helm/key-funder/templates/addresses-external-secret.yaml
  8. 18
      typescript/infra/helm/key-funder/templates/cron-job.yaml
  9. 4
      typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml
  10. 6
      typescript/infra/helm/key-funder/values.yaml
  11. 12
      typescript/infra/scripts/funding/deploy-key-funder.ts
  12. 471
      typescript/infra/scripts/funding/fund-keys-from-deployer.ts
  13. 328
      typescript/infra/scripts/funding/fund-relayers-from-deployer.ts
  14. 4
      typescript/infra/src/config/environment.ts
  15. 12
      typescript/infra/src/config/funding.ts
  16. 61
      typescript/infra/src/funding/deploy-key-funder.ts
  17. 61
      typescript/infra/src/funding/deploy-relayer-funder.ts
  18. 2
      typescript/sdk/src/gas/token-prices.ts

@ -1,9 +1,10 @@
import { RelayerFunderConfig } from '../../../src/config/funding';
import { KEY_ROLE_ENUM } from '../../../src/agents/roles';
import { KeyFunderConfig } from '../../../src/config/funding';
import { Contexts } from '../../contexts';
import { environment } from './chains';
export const relayerFunderConfig: RelayerFunderConfig = {
export const keyFunderConfig: KeyFunderConfig = {
docker: {
repo: 'gcr.io/abacus-labs-dev/abacus-monorepo',
tag: 'sha-d24eaa4',
@ -13,5 +14,7 @@ export const relayerFunderConfig: RelayerFunderConfig = {
prometheusPushGateway:
'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091',
contextFundingFrom: Contexts.Abacus,
contextsToFund: [Contexts.Abacus],
contextsAndRolesToFund: {
[Contexts.Abacus]: [KEY_ROLE_ENUM.Relayer],
},
};

@ -9,7 +9,7 @@ import {
mainnetConfigs,
} from './chains';
import { core } from './core';
import { relayerFunderConfig } from './funding';
import { keyFunderConfig } from './funding';
import { helloWorld } from './helloworld';
import { infrastructure } from './infrastructure';
@ -22,5 +22,5 @@ export const environment: CoreEnvironmentConfig<MainnetChains> = {
core,
infra: infrastructure,
helloWorld,
relayerFunderConfig,
keyFunderConfig,
};

@ -1,9 +1,10 @@
import { RelayerFunderConfig } from '../../../src/config/funding';
import { KEY_ROLE_ENUM } from '../../../src/agents/roles';
import { KeyFunderConfig } from '../../../src/config/funding';
import { Contexts } from '../../contexts';
import { environment } from './chains';
export const relayerFunderConfig: RelayerFunderConfig = {
export const keyFunderConfig: KeyFunderConfig = {
docker: {
repo: 'gcr.io/abacus-labs-dev/abacus-monorepo',
tag: 'sha-d24eaa4',
@ -13,5 +14,7 @@ export const relayerFunderConfig: RelayerFunderConfig = {
prometheusPushGateway:
'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091',
contextFundingFrom: Contexts.Abacus,
contextsToFund: [Contexts.Abacus],
contextsAndRolesToFund: {
[Contexts.Abacus]: [KEY_ROLE_ENUM.Relayer],
},
};

@ -9,7 +9,7 @@ import {
testnetConfigs,
} from './chains';
import { core } from './core';
import { relayerFunderConfig } from './funding';
import { keyFunderConfig } from './funding';
import { helloWorld } from './helloworld';
import { infrastructure } from './infrastructure';
@ -22,5 +22,5 @@ export const environment: CoreEnvironmentConfig<TestnetChains> = {
core,
infra: infrastructure,
helloWorld,
relayerFunderConfig,
keyFunderConfig,
};

@ -1,5 +1,5 @@
apiVersion: v2
name: relayer-funder
name: key-funder
description: Relayer funder
# A chart can be either an 'application' or a 'library' chart.

@ -1,7 +1,7 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: relayer-funder-addresses-external-secret
name: key-funder-addresses-external-secret
labels:
{{- include "abacus.labels" . | nindent 4 }}
spec:
@ -11,7 +11,7 @@ spec:
refreshInterval: "1h"
# The secret that will be created
target:
name: relayer-funder-addresses-secret
name: key-funder-addresses-secret
template:
type: Opaque
metadata:
@ -20,12 +20,12 @@ spec:
annotations:
update-on-redeploy: "{{ now }}"
data:
{{- range .Values.abacus.contextsToFund }}
{{ . }}-addresses.json: {{ printf "'{{ .%s_addresses | toString }}'" . }}
{{- range $context, $roles := .Values.abacus.contextsAndRoles }}
{{ $context }}-addresses.json: {{ printf "'{{ .%s_addresses | toString }}'" $context }}
{{- end }}
data:
{{- range .Values.abacus.contextsToFund }}
- secretKey: {{ . }}_addresses
{{- range $context, $roles := .Values.abacus.contextsAndRoles }}
- secretKey: {{ $context }}_addresses
remoteRef:
key: {{ printf "%s-%s-key-addresses" . $.Values.abacus.runEnv }}
key: {{ printf "%s-%s-key-addresses" $context $.Values.abacus.runEnv }}
{{- end }}

@ -1,7 +1,7 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: relayer-funder
name: key-funder
spec:
schedule: "{{ .Values.cronjob.schedule }}"
successfulJobsHistoryLimit: {{ .Values.cronjob.successfulJobsHistoryLimit }}
@ -14,7 +14,7 @@ spec:
spec:
restartPolicy: Never
containers:
- name: relayer-funder
- name: key-funder
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
imagePullPolicy: IfNotPresent
command:
@ -24,21 +24,23 @@ spec:
- {{ .Values.abacus.runEnv }}
- --context
- {{ .Values.abacus.contextFundingFrom }}
{{- range .Values.abacus.contextsToFund }}
{{- range $context, $roles := .Values.abacus.contextsAndRoles }}
- --contexts-and-roles
- {{ $context }}={{ join "," $roles }}
- -f
- /addresses-secret/{{ . }}-addresses.json
- /addresses-secret/{{ $context }}-addresses.json
{{- end }}
env:
- name: PROMETHEUS_PUSH_GATEWAY
value: {{ .Values.infra.prometheusPushGateway }}
envFrom:
- secretRef:
name: relayer-funder-env-var-secret
name: key-funder-env-var-secret
volumeMounts:
- name: relayer-funder-addresses-secret
- name: key-funder-addresses-secret
mountPath: /addresses-secret
volumes:
- name: relayer-funder-addresses-secret
- name: key-funder-addresses-secret
secret:
secretName: relayer-funder-addresses-secret
secretName: key-funder-addresses-secret
defaultMode: 0400

@ -1,7 +1,7 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: relayer-funder-env-var-external-secret
name: key-funder-env-var-external-secret
labels:
{{- include "abacus.labels" . | nindent 4 }}
spec:
@ -11,7 +11,7 @@ spec:
refreshInterval: "1h"
# The secret that will be created
target:
name: relayer-funder-env-var-secret
name: key-funder-env-var-secret
template:
type: Opaque
metadata:

@ -6,8 +6,10 @@ abacus:
# Used for fetching secrets
chains: []
contextFundingFrom: abacus
contextsToFund:
- abacus
# key = context, value = array of roles to fund
contextsAndRoles:
abacus:
- relayer
cronjob:
schedule: "*/10 * * * *" # Every 10 minutes
successfulJobsHistoryLimit: 1

@ -1,7 +1,7 @@
import {
getRelayerFunderConfig,
runRelayerFunderHelmCommand,
} from '../../src/funding/deploy-relayer-funder';
getKeyFunderConfig,
runKeyFunderHelmCommand,
} from '../../src/funding/deploy-key-funder';
import { HelmCommand } from '../../src/utils/helm';
import {
assertCorrectKubeContext,
@ -14,13 +14,13 @@ async function main() {
await assertCorrectKubeContext(coreConfig);
const relayerFunderConfig = getRelayerFunderConfig(coreConfig);
const keyFunderConfig = getKeyFunderConfig(coreConfig);
const agentConfig = await getContextAgentConfig(coreConfig);
await runRelayerFunderHelmCommand(
await runKeyFunderHelmCommand(
HelmCommand.InstallOrUpgrade,
agentConfig,
relayerFunderConfig,
keyFunderConfig,
);
}

@ -0,0 +1,471 @@
import { ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client';
import { format } from 'util';
import {
ChainConnection,
ChainName,
CompleteChainMap,
MultiProvider,
} from '@abacus-network/sdk';
import { error, log } from '@abacus-network/utils';
import { Contexts } from '../../config/contexts';
import { AgentKey, ReadOnlyAgentKey } from '../../src/agents/agent';
import { getAllKeys } from '../../src/agents/key-utils';
import { KEY_ROLE_ENUM } from '../../src/agents/roles';
import { ContextAndRoles, ContextAndRolesMap } from '../../src/config/funding';
import { submitMetrics } from '../../src/utils/metrics';
import {
assertContext,
assertRole,
readJSONAtPath,
} from '../../src/utils/utils';
import {
assertEnvironment,
getAgentConfig,
getArgs,
getCoreEnvironmentConfig,
} from '../utils';
const constMetricLabels = {
// this needs to get set in main because of async reasons
abacus_deployment: '',
abacus_context: 'abacus',
};
const metricsRegister = new Registry();
const walletBalanceGauge = new Gauge({
// Mirror the rust/ethers-prometheus `wallet_balance` gauge metric.
name: 'abacus_wallet_balance',
help: 'Current balance of eth and other tokens in the `tokens` map for the wallet addresses in the `wallets` set',
registers: [metricsRegister],
labelNames: [
'chain',
'wallet_address',
'wallet_name',
'token_address',
'token_symbol',
'token_name',
...(Object.keys(constMetricLabels) as (keyof typeof constMetricLabels)[]),
],
});
metricsRegister.registerMetric(walletBalanceGauge);
// Min delta is 1/10 of the desired balance
const MIN_DELTA_NUMERATOR = ethers.BigNumber.from(1);
const MIN_DELTA_DENOMINATOR = ethers.BigNumber.from(10);
const desiredBalancePerChain: CompleteChainMap<string> = {
celo: '0.1',
alfajores: '1',
avalanche: '0.1',
fuji: '1',
ethereum: '0.2',
kovan: '0.1',
polygon: '1',
mumbai: '0.5',
optimism: '0.05',
optimismkovan: '0.1',
arbitrum: '0.01',
arbitrumrinkeby: '0.1',
bsc: '0.01',
bsctestnet: '1',
// unused
goerli: '0',
auroratestnet: '0',
test1: '0',
test2: '0',
test3: '0',
};
// Funds key addresses for multiple contexts from the deployer key of the context
// specified via the `--context` flag.
// The --contexts-and-roles flag is used to specify the contexts and the key roles
// for each context to fund.
// There are two ways to configure this script so that key addresses are known.
// You can pass in files using `-f`, which are expected to each be JSON arrays of objects
// of the form { identifier: '..', address: '..' }, where the keys described in one file
// are all for the same context. This will avoid requiring any sort of GCP/AWS credentials for
// fetching addresses from the keys themselves. A file for each context specified in --contexts-and-roles
// must be provided
// If the -f flag is not provided, addresses will be read directly from GCP/AWS for each
// context provided in --contexts-and-roles, which requires the appropriate credentials.
//
// Example usage:
// ts-node ./scripts/funding/fund-keys-from-deployer.ts -e testnet2 --context abacus --contexts-and-roles abacus=relayer
async function main() {
const argv = await getArgs()
.string('f')
.array('f')
.alias('f', 'address-files')
.describe(
'f',
'Files each containing JSON arrays of identifier and address objects for a single context. If not specified, key addresses are fetched from GCP/AWS and require sufficient credentials.',
)
.string('contexts-and-roles')
.array('contexts-and-roles')
.describe(
'contexts-and-roles',
'Array indicating contexts and the roles to fund for each context. Each element is expected as <context>=<role>,<role>,<role>...',
)
.coerce('contexts-and-roles', parseContextAndRolesMap)
.demandOption('contexts-and-roles').argv;
const environment = assertEnvironment(argv.e as string);
constMetricLabels.abacus_deployment = environment;
const config = getCoreEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider();
let contextFunders: ContextFunder[];
if (argv.f) {
contextFunders = argv.f.map((path) =>
ContextFunder.fromSerializedAddressFile(
multiProvider,
path,
argv.contextsAndRoles,
),
);
} else {
contextFunders = [];
const contexts = Object.keys(argv.contextsAndRoles) as Contexts[];
contextFunders = await Promise.all(
contexts.map((context) =>
ContextFunder.fromContext(
multiProvider,
context,
argv.contextsAndRoles[context]!,
),
),
);
}
let failureOccurred = false;
for (const funder of contextFunders) {
const failure = await funder.fund();
if (failure) {
failureOccurred = true;
}
}
await submitMetrics(metricsRegister, 'key-funder');
if (failureOccurred) {
error('At least one failure occurred when funding');
process.exit(1);
}
}
// Funds keys for a single context
class ContextFunder {
public readonly chains: ChainName[];
constructor(
public readonly multiProvider: MultiProvider<any>,
public readonly keys: AgentKey[],
public readonly context: Contexts,
public readonly rolesToFund: KEY_ROLE_ENUM[],
) {
const uniqueChains = new Set(
keys.map((key) => key.chainName!).filter((chain) => chain !== undefined),
);
this.chains = Array.from(uniqueChains);
}
static fromSerializedAddressFile(
multiProvider: MultiProvider<any>,
path: string,
contextsAndRolesToFund: ContextAndRolesMap,
) {
log('Reading identifiers and addresses from file', {
path,
});
const idsAndAddresses = readJSONAtPath(path);
const keys: AgentKey[] = idsAndAddresses.map((idAndAddress: any) =>
ReadOnlyAgentKey.fromSerializedAddress(
idAndAddress.identifier,
idAndAddress.address,
),
);
const context = keys[0].context;
// Ensure all keys have the same context, just to be safe
for (const key of keys) {
if (key.context !== context) {
throw Error(
`Expected all keys at path ${path} to have context ${context}, found ${key.context}`,
);
}
}
const rolesToFund = contextsAndRolesToFund[context];
if (!rolesToFund) {
throw Error(
`Expected context ${context} to be defined in contextsAndRolesToFund`,
);
}
log('Read keys for context from file', {
path,
keyCount: keys.length,
context,
});
return new ContextFunder(multiProvider, keys, context, rolesToFund);
}
// The keys here are not ReadOnlyAgentKeys, instead they are AgentGCPKey or AgentAWSKeys,
// which require credentials to fetch. If you want to avoid requiring credentials, use
// fromSerializedAddressFile instead.
static async fromContext(
multiProvider: MultiProvider<any>,
context: Contexts,
rolesToFund: KEY_ROLE_ENUM[],
) {
const agentConfig = await getAgentConfig(context);
return new ContextFunder(
multiProvider,
getAllKeys(agentConfig),
context,
rolesToFund,
);
}
// Funds all the roles in this.rolesToFund
// Returns whether a failure occurred.
async fund(): Promise<boolean> {
let failureOccurred = false;
for (const role of this.rolesToFund) {
const failure =
role === KEY_ROLE_ENUM.Relayer
? await this.fundRelayersOnAllRequiredChains()
: await this.fundNonRelayerKeysOnAllChains(role);
if (failure) {
failureOccurred = true;
}
}
return failureOccurred;
}
// Returns whether a failure occurred.
private async fundNonRelayerKeysOnAllChains(
roleToFund: KEY_ROLE_ENUM,
): Promise<boolean> {
let failureOccurred = false;
const keys = this.getKeysWithRole(roleToFund);
for (const chain of this.chains) {
for (const key of keys) {
const failure = await this.attemptToFundKey(key, chain);
if (failure) {
failureOccurred = true;
}
}
}
return failureOccurred;
}
// Funds the relayers on all the chains found in `this.chains`.
// Does not fund a relayer key on its outbox chain.
// Returns whether a failure occurred.
private async fundRelayersOnAllRequiredChains(): Promise<boolean> {
let failureOccurred = false;
const keys = this.getKeysWithRole(KEY_ROLE_ENUM.Relayer);
for (const chain of this.chains) {
for (const key of keys.filter((k) => k.chainName !== chain)) {
const failure = await this.attemptToFundKey(key, chain);
if (failure) {
failureOccurred = true;
}
}
}
return failureOccurred;
}
private async attemptToFundKey(
key: AgentKey,
chain: ChainName,
): Promise<boolean> {
const chainConnection = this.multiProvider.getChainConnection(chain);
const desiredBalance = desiredBalancePerChain[chain];
let failureOccurred = false;
// Some types of keys must be fetched
await key.fetch();
try {
await this.fundKeyIfRequired(chainConnection, chain, key, desiredBalance);
} catch (err) {
error('Error funding key', {
key: getKeyInfo(key),
context: this.context,
error: err,
});
failureOccurred = true;
}
await this.updateWalletBalanceGauge(chainConnection, chain);
return failureOccurred;
}
// Tops up the key's balance to the desired balance if the current balance
// is lower than the desired balance by the min delta
private async fundKeyIfRequired(
chainConnection: ChainConnection,
chain: ChainName,
key: AgentKey,
desiredBalance: string,
) {
const currentBalance = await chainConnection.provider.getBalance(
key.address,
);
const desiredBalanceEther = ethers.utils.parseUnits(
desiredBalance,
'ether',
);
const delta = desiredBalanceEther.sub(currentBalance);
const minDelta = desiredBalanceEther
.mul(MIN_DELTA_NUMERATOR)
.div(MIN_DELTA_DENOMINATOR);
const keyInfo = getKeyInfo(key);
log('Assessing key for funding', {
key: keyInfo,
keyBalanceDelta: ethers.utils.formatEther(delta),
minKeyBalanceDelta: ethers.utils.formatEther(minDelta),
currentKeyBalance: ethers.utils.formatEther(currentBalance),
desiredKeyBalance: desiredBalance,
funder: {
address: await chainConnection.getAddress(),
balance: ethers.utils.formatEther(
await chainConnection.signer!.getBalance(),
),
},
context: this.context,
chain,
});
if (delta.gt(minDelta)) {
log('Sending funds...', {
key: keyInfo,
amount: ethers.utils.formatEther(delta),
context: this.context,
chain,
});
const tx = await chainConnection.signer!.sendTransaction({
to: key.address,
value: delta,
...chainConnection.overrides,
});
log('Sent transaction', {
key: keyInfo,
txUrl: chainConnection.getTxUrl(tx),
context: this.context,
chain,
});
const receipt = await tx.wait(chainConnection.confirmations);
log('Got transaction receipt', {
key: keyInfo,
receipt,
context: this.context,
chain,
});
}
}
private async updateWalletBalanceGauge(
chainConnection: ChainConnection,
chain: ChainName,
) {
const funderAddress = await chainConnection.getAddress();
walletBalanceGauge
.labels({
chain,
wallet_address: funderAddress ?? 'unknown',
wallet_name: 'key-funder',
token_symbol: 'Native',
token_name: 'Native',
...constMetricLabels,
})
.set(
parseFloat(
ethers.utils.formatEther(await chainConnection.signer!.getBalance()),
),
);
}
private getKeysWithRole(role: KEY_ROLE_ENUM) {
return this.keys.filter((k) => k.role === role);
}
}
function getKeyInfo(key: AgentKey) {
return {
context: key.context,
address: key.address,
identifier: key.identifier,
originChain: key.chainName,
role: key.role,
index: key.index,
};
}
function parseContextAndRolesMap(strs: string[]): ContextAndRolesMap {
const contextsAndRoles = strs.map(parseContextAndRoles);
return contextsAndRoles.reduce(
(prev, curr) => ({
...prev,
[curr.context]: curr.roles,
}),
{},
);
}
// Parses strings of the form <context>=<role>,<role>,<role>...
// e.g.:
// abacus=relayer
// flowcarbon=relayer,kathy
function parseContextAndRoles(str: string): ContextAndRoles {
const [contextStr, rolesStr] = str.split('=');
const context = assertContext(contextStr);
const roles = rolesStr.split(',').map(assertRole);
if (roles.length === 0) {
throw Error('Expected > 0 roles');
}
// For now, restrict the valid roles we think are reasonable to want to fund
const validRoles = new Set([KEY_ROLE_ENUM.Relayer, KEY_ROLE_ENUM.Kathy]);
for (const role of roles) {
if (!validRoles.has(role)) {
throw Error(
`Invalid role ${role}, must be one of ${Array.from(validRoles)}`,
);
}
}
return {
context,
roles,
};
}
main().catch((err) => {
error('Error occurred in main', {
// JSON.stringifying an Error returns '{}'.
// This is a workaround from https://stackoverflow.com/a/60370781
error: format(err),
});
process.exit(1);
});

@ -1,328 +0,0 @@
import { ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client';
import { format } from 'util';
import {
ChainConnection,
ChainName,
CompleteChainMap,
MultiProvider,
} from '@abacus-network/sdk';
import { error, log } from '@abacus-network/utils';
import { Contexts } from '../../config/contexts';
import { AgentKey, ReadOnlyAgentKey } from '../../src/agents/agent';
import { getRelayerKeys } from '../../src/agents/key-utils';
import { KEY_ROLE_ENUM } from '../../src/agents/roles';
import { submitMetrics } from '../../src/utils/metrics';
import { assertContext, readJSONAtPath } from '../../src/utils/utils';
import {
assertEnvironment,
getAgentConfig,
getArgs,
getCoreEnvironmentConfig,
} from '../utils';
const constMetricLabels = {
// this needs to get set in main because of async reasons
abacus_deployment: '',
abacus_context: 'abacus',
};
const metricsRegister = new Registry();
const walletBalanceGauge = new Gauge({
// Mirror the rust/ethers-prometheus `wallet_balance` gauge metric.
name: 'abacus_wallet_balance',
help: 'Current balance of eth and other tokens in the `tokens` map for the wallet addresses in the `wallets` set',
registers: [metricsRegister],
labelNames: [
'chain',
'wallet_address',
'wallet_name',
'token_address',
'token_symbol',
'token_name',
...(Object.keys(constMetricLabels) as (keyof typeof constMetricLabels)[]),
],
});
metricsRegister.registerMetric(walletBalanceGauge);
// Min delta is 1/10 of the desired balance
const MIN_DELTA_NUMERATOR = ethers.BigNumber.from(1);
const MIN_DELTA_DENOMINATOR = ethers.BigNumber.from(10);
const desiredBalancePerChain: CompleteChainMap<string> = {
celo: '0.1',
alfajores: '1',
avalanche: '0.1',
fuji: '1',
ethereum: '0.2',
kovan: '0.1',
polygon: '1',
mumbai: '0.5',
optimism: '0.05',
optimismkovan: '0.1',
arbitrum: '0.01',
arbitrumrinkeby: '0.1',
bsc: '0.01',
bsctestnet: '1',
// unused
goerli: '0',
auroratestnet: '0',
test1: '0',
test2: '0',
test3: '0',
};
// Funds relayer addresses for multiple contexts from the deployer key of the context
// specified via the `--context` flag.
// There are two ways to configure this script so that relayer addresses are known.
// You can pass in files using `-f`, which are expected to each be JSON arrays of objects
// of the form { identifier: '..', address: '..' }, where the keys described in one file
// are all for the same context. This will avoid requiring any sort of GCP/AWS credentials for
// fetching addresses from the keys themselves.
// Alternatively, using `--contexts-to-fund` will fetch relayer addresses from GCP/AWS, which
// requires credentials.
async function main() {
const argv = await getArgs()
.string('f')
.array('f')
.alias('f', 'address-files')
.describe(
'f',
'Files each containing JSON arrays of identifier and address objects for a context',
)
.string('contexts-to-fund')
.array('contexts-to-fund')
.describe(
'contexts-to-fund',
'Contexts to fund relayers for. If specified, relayer addresses are fetched from GCP/AWS and require sufficient credentials.',
)
.coerce('contexts-to-fund', (contexts: string[]) => {
return contexts.map(assertContext);
})
// Only one of the two methods for getting relayer addresses
.conflicts('f', 'contexts-to-fund').argv;
const environment = assertEnvironment(argv.e as string);
constMetricLabels.abacus_deployment = environment;
const config = getCoreEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider();
const contextRelayerFunders = argv.f
? argv.f.map((path) =>
ContextRelayerFunder.fromSerializedAddressFile(multiProvider, path),
)
: argv.contextsToFund!.map((context) =>
ContextRelayerFunder.fromSerializedAddressFile(multiProvider, context),
);
let failureOccurred = false;
for (const relayerFunder of contextRelayerFunders) {
const failure = await relayerFunder.fundRelayersOnAllChains();
if (failure) {
failureOccurred = true;
}
}
await submitMetrics(metricsRegister, 'relayer-funder');
if (failureOccurred) {
error('At least one failure occurred when funding relayers');
process.exit(1);
}
}
// Funds relayers for a single context
class ContextRelayerFunder {
public readonly chains: ChainName[];
constructor(
public readonly multiProvider: MultiProvider<any>,
public readonly keys: AgentKey[],
public readonly context: Contexts,
) {
this.chains = keys.map((key) => key.chainName!);
}
static fromSerializedAddressFile(
multiProvider: MultiProvider<any>,
path: string,
) {
log('Reading identifiers and addresses from file', {
path,
});
const idsAndAddresses = readJSONAtPath(path);
const keys: AgentKey[] = idsAndAddresses
.map((idAndAddress: any) =>
ReadOnlyAgentKey.fromSerializedAddress(
idAndAddress.identifier,
idAndAddress.address,
),
)
.filter((key: AgentKey) => key.role === KEY_ROLE_ENUM.Relayer);
const context = keys[0].context;
// Ensure all keys have the same context, just to be safe
keys.forEach((key) => {
if (key.context !== context) {
throw Error(
`Expected all keys at path ${path} to have context ${context}, found ${key.context}`,
);
}
});
log('Read keys for context from file', {
path,
keyCount: keys.length,
context,
});
return new ContextRelayerFunder(multiProvider, keys, context);
}
// The keys here are not ReadOnlyAgentKeys, instead they are AgentGCPKey or AgentAWSKeys,
// which require credentials to fetch. If you want to avoid requiring credentials, use
// fromSerializedAddressFile instead.
static async fromContext(
multiProvider: MultiProvider<any>,
context: Contexts,
) {
const agentConfig = await getAgentConfig(context);
return new ContextRelayerFunder(
multiProvider,
getRelayerKeys(agentConfig),
context,
);
}
// Funds the relayers on all the chains found in `this.chains`
async fundRelayersOnAllChains() {
let failureOccurred = false;
for (const chain of this.chains) {
const chainConnection = this.multiProvider.getChainConnection(chain);
const desiredBalance = desiredBalancePerChain[chain];
const funderAddress = await chainConnection.getAddress();
log('Funding relayers on chain...', {
chain,
funder: {
address: funderAddress,
balance: ethers.utils.formatEther(
await chainConnection.signer!.getBalance(),
),
desiredRelayerBalance: desiredBalance,
},
context: this.context,
});
for (const key of this.keys.filter((k) => k.chainName !== chain)) {
await key.fetch();
try {
await this.fundRelayerIfRequired(
chainConnection,
chain,
key,
desiredBalance,
);
} catch (err) {
error('Error funding relayer', {
relayer: relayerKeyInfo(key),
context: this.context,
error: err,
});
failureOccurred = true;
}
}
walletBalanceGauge
.labels({
chain,
wallet_address: funderAddress ?? 'unknown',
wallet_name: 'relayer-funder',
token_symbol: 'Native',
token_name: 'Native',
...constMetricLabels,
})
.set(
parseFloat(
ethers.utils.formatEther(
await chainConnection.signer!.getBalance(),
),
),
);
}
return failureOccurred;
}
private async fundRelayerIfRequired(
chainConnection: ChainConnection,
chain: ChainName,
key: AgentKey,
desiredBalance: string,
) {
const currentBalance = await chainConnection.provider.getBalance(
key.address,
);
const desiredBalanceEther = ethers.utils.parseUnits(
desiredBalance,
'ether',
);
const delta = desiredBalanceEther.sub(currentBalance);
const minDelta = desiredBalanceEther
.mul(MIN_DELTA_NUMERATOR)
.div(MIN_DELTA_DENOMINATOR);
const relayerInfo = relayerKeyInfo(key);
if (delta.gt(minDelta)) {
log('Sending relayer funds...', {
relayer: relayerInfo,
amount: ethers.utils.formatEther(delta),
context: this.context,
chain,
});
const tx = await chainConnection.signer!.sendTransaction({
to: key.address,
value: delta,
...chainConnection.overrides,
});
log('Sent transaction', {
relayer: relayerInfo,
txUrl: chainConnection.getTxUrl(tx),
context: this.context,
chain,
});
const receipt = await tx.wait(chainConnection.confirmations);
log('Got transaction receipt', {
relayer: relayerInfo,
receipt,
context: this.context,
chain,
});
}
}
}
function relayerKeyInfo(relayerKey: AgentKey) {
return {
context: relayerKey.context,
address: relayerKey.address,
identifier: relayerKey.identifier,
originChain: relayerKey.chainName,
};
}
main().catch((err) => {
error('Error occurred in main', {
// JSON.stringifying an Error returns '{}'.
// This is a workaround from https://stackoverflow.com/a/60370781
error: format(err),
});
process.exit(1);
});

@ -10,7 +10,7 @@ import { Contexts } from '../../config/contexts';
import { environments } from '../../config/environments';
import { AgentConfig } from './agent';
import { RelayerFunderConfig } from './funding';
import { KeyFunderConfig } from './funding';
import { HelloWorldConfig } from './helloworld';
import { InfrastructureConfig } from './infrastructure';
@ -30,5 +30,5 @@ export type CoreEnvironmentConfig<Chain extends ChainName> = {
infra: InfrastructureConfig;
getMultiProvider: (context?: Contexts) => Promise<MultiProvider<Chain>>;
helloWorld?: HelloWorldConfig<Chain>;
relayerFunderConfig?: RelayerFunderConfig;
keyFunderConfig?: KeyFunderConfig;
};

@ -1,12 +1,20 @@
import { Contexts } from '../../config/contexts';
import { KEY_ROLE_ENUM } from '../agents/roles';
import { DockerConfig } from './agent';
export interface RelayerFunderConfig {
export interface ContextAndRoles {
context: Contexts;
roles: KEY_ROLE_ENUM[];
}
export type ContextAndRolesMap = Partial<Record<Contexts, KEY_ROLE_ENUM[]>>;
export interface KeyFunderConfig {
docker: DockerConfig;
cronSchedule: string;
namespace: string;
contextFundingFrom: Contexts;
contextsToFund: Contexts[];
contextsAndRolesToFund: ContextAndRolesMap;
prometheusPushGateway: string;
}

@ -0,0 +1,61 @@
import { ChainName } from '@abacus-network/sdk';
import { AgentConfig, CoreEnvironmentConfig } from '../config';
import { KeyFunderConfig } from '../config/funding';
import { HelmCommand, helmifyValues } from '../utils/helm';
import { execCmd } from '../utils/utils';
export function runKeyFunderHelmCommand<Chain extends ChainName>(
helmCommand: HelmCommand,
agentConfig: AgentConfig<Chain>,
keyFunderConfig: KeyFunderConfig,
) {
const values = getKeyFunderHelmValues(agentConfig, keyFunderConfig);
return execCmd(
`helm ${helmCommand} key-funder ./helm/key-funder --namespace ${
keyFunderConfig.namespace
} ${values.join(' ')}`,
{},
false,
true,
);
}
function getKeyFunderHelmValues<Chain extends ChainName>(
agentConfig: AgentConfig<Chain>,
keyFunderConfig: KeyFunderConfig,
) {
const values = {
cronjob: {
schedule: keyFunderConfig.cronSchedule,
},
abacus: {
runEnv: agentConfig.environment,
// Only used for fetching RPC urls as env vars
chains: agentConfig.contextChainNames,
contextFundingFrom: keyFunderConfig.contextFundingFrom,
contextsAndRolesToFund: keyFunderConfig.contextsAndRolesToFund,
},
image: {
repository: keyFunderConfig.docker.repo,
tag: keyFunderConfig.docker.tag,
},
infra: {
prometheusPushGateway: keyFunderConfig.prometheusPushGateway,
},
};
return helmifyValues(values);
}
export function getKeyFunderConfig(
coreConfig: CoreEnvironmentConfig<any>,
): KeyFunderConfig {
const keyFunderConfig = coreConfig.keyFunderConfig;
if (!keyFunderConfig) {
throw new Error(
`Environment ${coreConfig.environment} does not have a KeyFunderConfig config`,
);
}
return keyFunderConfig;
}

@ -1,61 +0,0 @@
import { ChainName } from '@abacus-network/sdk';
import { AgentConfig, CoreEnvironmentConfig } from '../config';
import { RelayerFunderConfig } from '../config/funding';
import { HelmCommand, helmifyValues } from '../utils/helm';
import { execCmd } from '../utils/utils';
export function runRelayerFunderHelmCommand<Chain extends ChainName>(
helmCommand: HelmCommand,
agentConfig: AgentConfig<Chain>,
relayerFunderConfig: RelayerFunderConfig,
) {
const values = getRelayerFunderHelmValues(agentConfig, relayerFunderConfig);
return execCmd(
`helm ${helmCommand} relayer-funder ./helm/relayer-funder --namespace ${
relayerFunderConfig.namespace
} ${values.join(' ')}`,
{},
false,
true,
);
}
function getRelayerFunderHelmValues<Chain extends ChainName>(
agentConfig: AgentConfig<Chain>,
relayerFunderConfig: RelayerFunderConfig,
) {
const values = {
cronjob: {
schedule: relayerFunderConfig.cronSchedule,
},
abacus: {
runEnv: agentConfig.environment,
// Only used for fetching RPC urls as env vars
chains: agentConfig.contextChainNames,
contextFundingFrom: relayerFunderConfig.contextFundingFrom,
contextsToFund: relayerFunderConfig.contextsToFund,
},
image: {
repository: relayerFunderConfig.docker.repo,
tag: relayerFunderConfig.docker.tag,
},
infra: {
prometheusPushGateway: relayerFunderConfig.prometheusPushGateway,
},
};
return helmifyValues(values);
}
export function getRelayerFunderConfig(
coreConfig: CoreEnvironmentConfig<any>,
): RelayerFunderConfig {
const relayerFunderConfig = coreConfig.relayerFunderConfig;
if (!relayerFunderConfig) {
throw new Error(
`Environment ${coreConfig.environment} does not have a RelayerFunderConfig config`,
);
}
return relayerFunderConfig;
}

@ -43,7 +43,7 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
const allTestnets = isMainnet.every((v) => v === false);
if (allTestnets) {
// Testnet tokens are all artificially priced at 1.0 USD.
return chains.map((_) => 1);
return chains.map(() => 1);
}
if (!allMainnets) {

Loading…
Cancel
Save