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

@ -9,7 +9,7 @@ import {
mainnetConfigs, mainnetConfigs,
} from './chains'; } from './chains';
import { core } from './core'; import { core } from './core';
import { relayerFunderConfig } from './funding'; import { keyFunderConfig } from './funding';
import { helloWorld } from './helloworld'; import { helloWorld } from './helloworld';
import { infrastructure } from './infrastructure'; import { infrastructure } from './infrastructure';
@ -22,5 +22,5 @@ export const environment: CoreEnvironmentConfig<MainnetChains> = {
core, core,
infra: infrastructure, infra: infrastructure,
helloWorld, 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 { Contexts } from '../../contexts';
import { environment } from './chains'; import { environment } from './chains';
export const relayerFunderConfig: RelayerFunderConfig = { export const keyFunderConfig: KeyFunderConfig = {
docker: { docker: {
repo: 'gcr.io/abacus-labs-dev/abacus-monorepo', repo: 'gcr.io/abacus-labs-dev/abacus-monorepo',
tag: 'sha-d24eaa4', tag: 'sha-d24eaa4',
@ -13,5 +14,7 @@ export const relayerFunderConfig: RelayerFunderConfig = {
prometheusPushGateway: prometheusPushGateway:
'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091', 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091',
contextFundingFrom: Contexts.Abacus, contextFundingFrom: Contexts.Abacus,
contextsToFund: [Contexts.Abacus], contextsAndRolesToFund: {
[Contexts.Abacus]: [KEY_ROLE_ENUM.Relayer],
},
}; };

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

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

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

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

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

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

@ -1,7 +1,7 @@
import { import {
getRelayerFunderConfig, getKeyFunderConfig,
runRelayerFunderHelmCommand, runKeyFunderHelmCommand,
} from '../../src/funding/deploy-relayer-funder'; } from '../../src/funding/deploy-key-funder';
import { HelmCommand } from '../../src/utils/helm'; import { HelmCommand } from '../../src/utils/helm';
import { import {
assertCorrectKubeContext, assertCorrectKubeContext,
@ -14,13 +14,13 @@ async function main() {
await assertCorrectKubeContext(coreConfig); await assertCorrectKubeContext(coreConfig);
const relayerFunderConfig = getRelayerFunderConfig(coreConfig); const keyFunderConfig = getKeyFunderConfig(coreConfig);
const agentConfig = await getContextAgentConfig(coreConfig); const agentConfig = await getContextAgentConfig(coreConfig);
await runRelayerFunderHelmCommand( await runKeyFunderHelmCommand(
HelmCommand.InstallOrUpgrade, HelmCommand.InstallOrUpgrade,
agentConfig, 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 { environments } from '../../config/environments';
import { AgentConfig } from './agent'; import { AgentConfig } from './agent';
import { RelayerFunderConfig } from './funding'; import { KeyFunderConfig } from './funding';
import { HelloWorldConfig } from './helloworld'; import { HelloWorldConfig } from './helloworld';
import { InfrastructureConfig } from './infrastructure'; import { InfrastructureConfig } from './infrastructure';
@ -30,5 +30,5 @@ export type CoreEnvironmentConfig<Chain extends ChainName> = {
infra: InfrastructureConfig; infra: InfrastructureConfig;
getMultiProvider: (context?: Contexts) => Promise<MultiProvider<Chain>>; getMultiProvider: (context?: Contexts) => Promise<MultiProvider<Chain>>;
helloWorld?: HelloWorldConfig<Chain>; helloWorld?: HelloWorldConfig<Chain>;
relayerFunderConfig?: RelayerFunderConfig; keyFunderConfig?: KeyFunderConfig;
}; };

@ -1,12 +1,20 @@
import { Contexts } from '../../config/contexts'; import { Contexts } from '../../config/contexts';
import { KEY_ROLE_ENUM } from '../agents/roles';
import { DockerConfig } from './agent'; 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; docker: DockerConfig;
cronSchedule: string; cronSchedule: string;
namespace: string; namespace: string;
contextFundingFrom: Contexts; contextFundingFrom: Contexts;
contextsToFund: Contexts[]; contextsAndRolesToFund: ContextAndRolesMap;
prometheusPushGateway: string; 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); const allTestnets = isMainnet.every((v) => v === false);
if (allTestnets) { if (allTestnets) {
// Testnet tokens are all artificially priced at 1.0 USD. // Testnet tokens are all artificially priced at 1.0 USD.
return chains.map((_) => 1); return chains.map(() => 1);
} }
if (!allMainnets) { if (!allMainnets) {

Loading…
Cancel
Save