commit
d7745e8942
@ -0,0 +1,7 @@ |
||||
--- |
||||
'@hyperlane-xyz/infra': minor |
||||
'@hyperlane-xyz/cli': minor |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Add support for updating the mailbox proxy admin owner |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/sdk': minor |
||||
--- |
||||
|
||||
Added coinGeckoId as an optional property of the TokenConfigSchema |
@ -0,0 +1,5 @@ |
||||
--- |
||||
'@hyperlane-xyz/sdk': major |
||||
--- |
||||
|
||||
Remove getCoingeckoTokenPrices (use CoinGeckoTokenPriceGetter instead) |
@ -0,0 +1,199 @@ |
||||
import { expect } from 'chai'; |
||||
import { Signer, Wallet, ethers } from 'ethers'; |
||||
|
||||
import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; |
||||
import { |
||||
CoreConfig, |
||||
ProtocolFeeHookConfig, |
||||
randomAddress, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { Address } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; |
||||
|
||||
import { |
||||
hyperlaneCoreApply, |
||||
hyperlaneCoreDeploy, |
||||
readCoreConfig, |
||||
} from './commands/core.js'; |
||||
import { ANVIL_KEY, REGISTRY_PATH } from './commands/helpers.js'; |
||||
|
||||
const CHAIN_NAME = 'anvil2'; |
||||
|
||||
const EXAMPLES_PATH = './examples'; |
||||
const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`; |
||||
|
||||
const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh
|
||||
const CORE_READ_CONFIG_PATH = `${TEMP_PATH}/anvil2/core-config-read.yaml`; |
||||
|
||||
const TEST_TIMEOUT = 100_000; // Long timeout since these tests can take a while
|
||||
describe('hyperlane core e2e tests', async function () { |
||||
this.timeout(TEST_TIMEOUT); |
||||
|
||||
let signer: Signer; |
||||
let initialOwnerAddress: Address; |
||||
|
||||
before(async () => { |
||||
const chainMetadata: any = readYamlOrJson( |
||||
`${REGISTRY_PATH}/chains/${CHAIN_NAME}/metadata.yaml`, |
||||
); |
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider( |
||||
chainMetadata.rpcUrls[0].http, |
||||
); |
||||
|
||||
const wallet = new Wallet(ANVIL_KEY); |
||||
signer = wallet.connect(provider); |
||||
|
||||
initialOwnerAddress = await signer.getAddress(); |
||||
}); |
||||
|
||||
describe('core.deploy', () => { |
||||
it('should create a core deployment with the signer as the mailbox owner', async () => { |
||||
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); |
||||
|
||||
const coreConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
|
||||
expect(coreConfig.owner).to.equal(initialOwnerAddress); |
||||
expect(coreConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress); |
||||
// Assuming that the ProtocolFeeHook is used for deployment
|
||||
expect((coreConfig.requiredHook as ProtocolFeeHookConfig).owner).to.equal( |
||||
initialOwnerAddress, |
||||
); |
||||
}); |
||||
|
||||
it('should create a core deployment with the mailbox owner set to the address in the config', async () => { |
||||
const coreConfig: CoreConfig = await readYamlOrJson(CORE_CONFIG_PATH); |
||||
|
||||
const newOwner = randomAddress().toLowerCase(); |
||||
|
||||
coreConfig.owner = newOwner; |
||||
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); |
||||
|
||||
// Deploy the core contracts with the updated mailbox owner
|
||||
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_READ_CONFIG_PATH); |
||||
|
||||
// Verify that the owner has been set correctly without modifying any other owner values
|
||||
const updatedConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
|
||||
expect(updatedConfig.owner.toLowerCase()).to.equal(newOwner); |
||||
expect(updatedConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress); |
||||
// Assuming that the ProtocolFeeHook is used for deployment
|
||||
expect( |
||||
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, |
||||
).to.equal(initialOwnerAddress); |
||||
}); |
||||
|
||||
it('should create a core deployment with ProxyAdmin owner of the mailbox set to the address in the config', async () => { |
||||
const coreConfig: CoreConfig = await readYamlOrJson(CORE_CONFIG_PATH); |
||||
|
||||
const newOwner = randomAddress().toLowerCase(); |
||||
|
||||
coreConfig.proxyAdmin = { owner: newOwner }; |
||||
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); |
||||
|
||||
// Deploy the core contracts with the updated mailbox owner
|
||||
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_READ_CONFIG_PATH); |
||||
|
||||
// Verify that the owner has been set correctly without modifying any other owner values
|
||||
const updatedConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
|
||||
expect(updatedConfig.owner).to.equal(initialOwnerAddress); |
||||
expect(updatedConfig.proxyAdmin?.owner.toLowerCase()).to.equal(newOwner); |
||||
// Assuming that the ProtocolFeeHook is used for deployment
|
||||
expect( |
||||
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, |
||||
).to.equal(initialOwnerAddress); |
||||
}); |
||||
}); |
||||
|
||||
describe('core.apply', () => { |
||||
it('should update the mailbox owner', async () => { |
||||
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); |
||||
const coreConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
expect(coreConfig.owner).to.equal(initialOwnerAddress); |
||||
const newOwner = randomAddress().toLowerCase(); |
||||
coreConfig.owner = newOwner; |
||||
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); |
||||
await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH); |
||||
// Verify that the owner has been set correctly without modifying any other owner values
|
||||
const updatedConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
expect(updatedConfig.owner.toLowerCase()).to.equal(newOwner); |
||||
expect(updatedConfig.proxyAdmin?.owner).to.equal(initialOwnerAddress); |
||||
// Assuming that the ProtocolFeeHook is used for deployment
|
||||
expect( |
||||
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, |
||||
).to.equal(initialOwnerAddress); |
||||
}); |
||||
|
||||
it('should update the ProxyAdmin to a new one for the mailbox', async () => { |
||||
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); |
||||
const coreConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
expect(coreConfig.owner).to.equal(initialOwnerAddress); |
||||
|
||||
const proxyFactory = new ProxyAdmin__factory().connect(signer); |
||||
const deployTx = await proxyFactory.deploy(); |
||||
const newProxyAdmin = await deployTx.deployed(); |
||||
coreConfig.proxyAdmin!.address = newProxyAdmin.address; |
||||
|
||||
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); |
||||
await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH); |
||||
|
||||
// Verify that the owner has been set correctly without modifying any other owner values
|
||||
const updatedConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
expect(updatedConfig.owner).to.equal(initialOwnerAddress); |
||||
expect(updatedConfig.proxyAdmin?.address).to.equal(newProxyAdmin.address); |
||||
// Assuming that the ProtocolFeeHook is used for deployment
|
||||
expect( |
||||
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, |
||||
).to.equal(initialOwnerAddress); |
||||
}); |
||||
|
||||
it('should update the ProxyAdmin owner for the mailbox', async () => { |
||||
await hyperlaneCoreDeploy(CHAIN_NAME, CORE_CONFIG_PATH); |
||||
const coreConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
expect(coreConfig.owner).to.equal(initialOwnerAddress); |
||||
|
||||
const newOwner = randomAddress().toLowerCase(); |
||||
coreConfig.proxyAdmin!.owner = newOwner; |
||||
writeYamlOrJson(CORE_READ_CONFIG_PATH, coreConfig); |
||||
await hyperlaneCoreApply(CHAIN_NAME, CORE_READ_CONFIG_PATH); |
||||
|
||||
// Verify that the owner has been set correctly without modifying any other owner values
|
||||
const updatedConfig: CoreConfig = await readCoreConfig( |
||||
CHAIN_NAME, |
||||
CORE_READ_CONFIG_PATH, |
||||
); |
||||
expect(updatedConfig.owner).to.equal(initialOwnerAddress); |
||||
expect(updatedConfig.proxyAdmin?.owner.toLowerCase()).to.equal(newOwner); |
||||
// Assuming that the ProtocolFeeHook is used for deployment
|
||||
expect( |
||||
(updatedConfig.requiredHook as ProtocolFeeHookConfig).owner, |
||||
).to.equal(initialOwnerAddress); |
||||
}); |
||||
}); |
||||
}); |
@ -1,51 +1,83 @@ |
||||
import { expect } from 'chai'; |
||||
import sinon from 'sinon'; |
||||
|
||||
import { ethereum, solanamainnet } from '@hyperlane-xyz/registry'; |
||||
|
||||
import { TestChainName, testChainMetadata } from '../consts/testChains.js'; |
||||
import { MockCoinGecko } from '../test/MockCoinGecko.js'; |
||||
|
||||
import { CoinGeckoTokenPriceGetter } from './token-prices.js'; |
||||
|
||||
const MOCK_FETCH_CALLS = true; |
||||
|
||||
describe('TokenPriceGetter', () => { |
||||
let tokenPriceGetter: CoinGeckoTokenPriceGetter; |
||||
let mockCoinGecko: MockCoinGecko; |
||||
const chainA = TestChainName.test1, |
||||
chainB = TestChainName.test2, |
||||
priceA = 1, |
||||
priceB = 5.5; |
||||
before(async () => { |
||||
mockCoinGecko = new MockCoinGecko(); |
||||
// Origin token
|
||||
mockCoinGecko.setTokenPrice(chainA, priceA); |
||||
// Destination token
|
||||
mockCoinGecko.setTokenPrice(chainB, priceB); |
||||
tokenPriceGetter = new CoinGeckoTokenPriceGetter( |
||||
mockCoinGecko, |
||||
testChainMetadata, |
||||
undefined, |
||||
0, |
||||
); |
||||
|
||||
const chainA = TestChainName.test1; |
||||
const chainB = TestChainName.test2; |
||||
const priceA = 2; |
||||
const priceB = 5; |
||||
let stub: sinon.SinonStub; |
||||
|
||||
beforeEach(() => { |
||||
tokenPriceGetter = new CoinGeckoTokenPriceGetter({ |
||||
chainMetadata: { ethereum, solanamainnet, ...testChainMetadata }, |
||||
apiKey: 'test', |
||||
expirySeconds: 10, |
||||
sleepMsBetweenRequests: 10, |
||||
}); |
||||
|
||||
describe('getTokenPrice', () => { |
||||
it('returns a token price', async () => { |
||||
expect(await tokenPriceGetter.getTokenPrice(chainA)).to.equal(priceA); |
||||
if (MOCK_FETCH_CALLS) { |
||||
stub = sinon |
||||
.stub(tokenPriceGetter, 'fetchPriceData') |
||||
.returns(Promise.resolve([priceA, priceB])); |
||||
} |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
if (MOCK_FETCH_CALLS && stub) { |
||||
stub.restore(); |
||||
} |
||||
}); |
||||
|
||||
it('caches a token price', async () => { |
||||
mockCoinGecko.setFail(chainA, true); |
||||
expect(await tokenPriceGetter.getTokenPrice(chainA)).to.equal(priceA); |
||||
mockCoinGecko.setFail(chainA, false); |
||||
describe('getTokenPriceByIds', () => { |
||||
it('returns token prices', async () => { |
||||
// stubbed results
|
||||
expect( |
||||
await tokenPriceGetter.getTokenPriceByIds([ |
||||
ethereum.name, |
||||
solanamainnet.name, |
||||
]), |
||||
).to.eql([priceA, priceB]); |
||||
}); |
||||
}); |
||||
|
||||
describe('getTokenPrice', () => { |
||||
it('returns a token price', async () => { |
||||
// hardcoded result of 1 for testnets
|
||||
expect( |
||||
await tokenPriceGetter.getTokenPrice(TestChainName.test1), |
||||
).to.equal(1); |
||||
// stubbed result for non-testnet
|
||||
expect(await tokenPriceGetter.getTokenPrice(ethereum.name)).to.equal( |
||||
priceA, |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('getTokenExchangeRate', () => { |
||||
it('returns a value consistent with getTokenPrice()', async () => { |
||||
const exchangeRate = await tokenPriceGetter.getTokenExchangeRate( |
||||
chainA, |
||||
chainB, |
||||
); |
||||
// Should equal 1 because testnet prices are always forced to 1
|
||||
expect(exchangeRate).to.equal(1); |
||||
// hardcoded result of 1 for testnets
|
||||
expect( |
||||
await tokenPriceGetter.getTokenExchangeRate(chainA, chainB), |
||||
).to.equal(1); |
||||
|
||||
// stubbed result for non-testnet
|
||||
expect( |
||||
await tokenPriceGetter.getTokenExchangeRate( |
||||
ethereum.name, |
||||
solanamainnet.name, |
||||
), |
||||
).to.equal(priceA / priceB); |
||||
}); |
||||
}); |
||||
}); |
||||
|
@ -1,47 +0,0 @@ |
||||
import { SimplePriceResponse } from 'coingecko-api-v3'; |
||||
|
||||
import type { |
||||
CoinGeckoInterface, |
||||
CoinGeckoResponse, |
||||
CoinGeckoSimplePriceInterface, |
||||
CoinGeckoSimplePriceParams, |
||||
} from '../gas/token-prices.js'; |
||||
import type { ChainName } from '../types.js'; |
||||
|
||||
// A mock CoinGecko intended to be used by tests
|
||||
export class MockCoinGecko implements CoinGeckoInterface { |
||||
// Prices keyed by coingecko id
|
||||
private tokenPrices: Record<string, number>; |
||||
// Whether or not to fail to return a response, keyed by coingecko id
|
||||
private fail: Record<string, boolean>; |
||||
|
||||
constructor() { |
||||
this.tokenPrices = {}; |
||||
this.fail = {}; |
||||
} |
||||
|
||||
price(input: CoinGeckoSimplePriceParams): CoinGeckoResponse { |
||||
const data: SimplePriceResponse = {}; |
||||
for (const id of input.ids) { |
||||
if (this.fail[id]) { |
||||
return Promise.reject(`Failed to fetch price for ${id}`); |
||||
} |
||||
data[id] = { |
||||
usd: this.tokenPrices[id], |
||||
}; |
||||
} |
||||
return Promise.resolve(data); |
||||
} |
||||
|
||||
get simplePrice(): CoinGeckoSimplePriceInterface { |
||||
return this.price; |
||||
} |
||||
|
||||
setTokenPrice(chain: ChainName, price: number): void { |
||||
this.tokenPrices[chain] = price; |
||||
} |
||||
|
||||
setFail(chain: ChainName, fail: boolean): void { |
||||
this.fail[chain] = fail; |
||||
} |
||||
} |
@ -1,6 +1,18 @@ |
||||
{ |
||||
"extends": [ |
||||
"eslint:recommended", |
||||
"plugin:@typescript-eslint/recommended", |
||||
"plugin:react/recommended", |
||||
"plugin:react-hooks/recommended", |
||||
"prettier" |
||||
], |
||||
"plugins": ["react", "react-hooks", "@typescript-eslint"], |
||||
"rules": { |
||||
// TODO use utils rootLogger in widgets lib |
||||
"no-console": ["off"] |
||||
"no-console": ["off"], |
||||
"react/react-in-jsx-scope": "off", |
||||
"react/prop-types": "off", |
||||
"react-hooks/rules-of-hooks": "error", |
||||
"react-hooks/exhaustive-deps": "warn" |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue