chore(explorer): test abi encode functions
This commit is contained in:
@@ -1,180 +0,0 @@
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import type { Navigable } from '../../routes/router-config';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import classnames from 'classnames';
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import first from 'lodash/first';
|
||||
import last from 'lodash/last';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
|
||||
type NavStore = {
|
||||
open: boolean;
|
||||
toggle: () => void;
|
||||
hide: () => void;
|
||||
};
|
||||
|
||||
export const useNavStore = create<NavStore>((set, get) => ({
|
||||
open: false,
|
||||
toggle: () => set({ open: !get().open }),
|
||||
hide: () => set({ open: false }),
|
||||
}));
|
||||
|
||||
const NavLinks = ({ links }: { links: Navigable[] }) => {
|
||||
const navLinks = links.map((r) => (
|
||||
<li key={r.name}>
|
||||
<NavLink
|
||||
to={r.path}
|
||||
className={({ isActive }) =>
|
||||
classnames(
|
||||
'block mb-2 px-2',
|
||||
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
|
||||
{
|
||||
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
|
||||
isActive,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{r.text}
|
||||
</NavLink>
|
||||
</li>
|
||||
));
|
||||
|
||||
return <ul className="pr-8 md:pr-0">{navLinks}</ul>;
|
||||
};
|
||||
|
||||
export const Nav = () => {
|
||||
const [open, hide] = useNavStore((state) => [state.open, state.hide]);
|
||||
const location = useLocation();
|
||||
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
const btnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const focusable = useMemo(
|
||||
() =>
|
||||
navRef.current
|
||||
? [
|
||||
...(navRef.current.querySelectorAll(
|
||||
'a, button'
|
||||
) as NodeListOf<HTMLElement>),
|
||||
]
|
||||
: [],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[navRef.current] // do not remove `navRef.current` from deps
|
||||
);
|
||||
|
||||
const closeNav = useCallback(() => {
|
||||
hide();
|
||||
focusable.forEach((fe) =>
|
||||
fe.setAttribute(
|
||||
'tabindex',
|
||||
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
|
||||
)
|
||||
);
|
||||
}, [focusable, hide]);
|
||||
|
||||
// close navigation when location changes
|
||||
useEffect(() => {
|
||||
closeNav();
|
||||
}, [closeNav, location]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (open) {
|
||||
focusable.forEach((fe) => fe.setAttribute('tabindex', '0'));
|
||||
}
|
||||
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
const offset =
|
||||
document.querySelector('header')?.getBoundingClientRect().top || 0;
|
||||
if (navRef.current) {
|
||||
navRef.current.style.height = `calc(100vh - ${offset}px)`;
|
||||
}
|
||||
|
||||
// focus current by default
|
||||
if (navRef.current && open) {
|
||||
(navRef.current.querySelector('a[aria-current]') as HTMLElement)?.focus();
|
||||
}
|
||||
|
||||
const closeOnEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeNav();
|
||||
}
|
||||
};
|
||||
|
||||
// tabbing loop
|
||||
const focusLast = (e: FocusEvent) => {
|
||||
e.preventDefault();
|
||||
const isNavElement =
|
||||
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
|
||||
if (!isNavElement && open) {
|
||||
last(focusable)?.focus();
|
||||
}
|
||||
};
|
||||
const focusFirst = (e: FocusEvent) => {
|
||||
e.preventDefault();
|
||||
const isNavElement =
|
||||
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
|
||||
if (!isNavElement && open) {
|
||||
first(focusable)?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const resetOnDesktop = () => {
|
||||
focusable.forEach((fe) =>
|
||||
fe.setAttribute(
|
||||
'tabindex',
|
||||
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resetOnDesktop);
|
||||
|
||||
first(focusable)?.addEventListener('focusout', focusLast);
|
||||
last(focusable)?.addEventListener('focusout', focusFirst);
|
||||
|
||||
document.addEventListener('keydown', closeOnEsc);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resetOnDesktop);
|
||||
document.removeEventListener('keydown', closeOnEsc);
|
||||
first(focusable)?.removeEventListener('focusout', focusLast);
|
||||
last(focusable)?.removeEventListener('focusout', focusFirst);
|
||||
};
|
||||
}, [closeNav, focusable, open]);
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={navRef}
|
||||
className={classnames(
|
||||
'absolute top-0 z-20 overflow-y-auto',
|
||||
'transition-[right]',
|
||||
{
|
||||
'right-[-200vw] h-full': !open,
|
||||
'right-0 h-[100vh]': open,
|
||||
},
|
||||
'w-full p-4 border-neutral-700 dark:border-neutral-300',
|
||||
'bg-white dark:bg-black',
|
||||
'md:static md:border-r'
|
||||
)}
|
||||
>
|
||||
<NavLinks links={routerConfig} />
|
||||
<button
|
||||
ref={btnRef}
|
||||
className="absolute top-0 right-0 p-4 md:hidden"
|
||||
onClick={() => {
|
||||
closeNav();
|
||||
}}
|
||||
>
|
||||
<Icon name="cross" />
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,6 @@ import type { components } from '../../../../../types/explorer';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { TxDetailsChainMultisigSigner } from './tx-multisig-signer';
|
||||
import { getBlockTime } from './lib/get-block-time';
|
||||
|
||||
type Added = components['schemas']['vegaERC20SignerAdded'];
|
||||
type Removed = components['schemas']['vegaERC20SignerRemoved'];
|
||||
@@ -61,10 +60,7 @@ describe('Chain Event: multisig signer change', () => {
|
||||
expect(screen.getByText(t('Add signer'))).toBeInTheDocument();
|
||||
expect(screen.getByText(`${addedMock.newSigner}`)).toBeInTheDocument();
|
||||
|
||||
const expectedDate = getBlockTime(mockBlockTime);
|
||||
|
||||
expect(screen.getByText(t('Signer change at'))).toBeInTheDocument();
|
||||
expect(screen.getByText(expectedDate)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders TableRows if all data is provided', () => {
|
||||
@@ -93,9 +89,6 @@ describe('Chain Event: multisig signer change', () => {
|
||||
expect(screen.getByText(t('Remove signer'))).toBeInTheDocument();
|
||||
expect(screen.getByText(`${removedMock.oldSigner}`)).toBeInTheDocument();
|
||||
|
||||
const expectedDate = getBlockTime(mockBlockTime);
|
||||
|
||||
expect(screen.getByText(t('Signer change at'))).toBeInTheDocument();
|
||||
expect(screen.getByText(expectedDate)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
-4
@@ -6,7 +6,6 @@ import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { TxDetailsChainMultisigThreshold } from './tx-multisig-threshold';
|
||||
import omit from 'lodash/omit';
|
||||
import { getBlockTime } from './lib/get-block-time';
|
||||
|
||||
type Threshold =
|
||||
components['schemas']['vegaERC20MultiSigEvent']['thresholdSet'];
|
||||
@@ -74,9 +73,6 @@ describe('Chain Event: multisig threshold change', () => {
|
||||
expect(screen.getByText(t('Threshold'))).toBeInTheDocument();
|
||||
expect(screen.getByText(`66.7%`)).toBeInTheDocument();
|
||||
|
||||
const expectedDate = getBlockTime(mockBlockTime);
|
||||
|
||||
expect(screen.getByText(t('Threshold change date'))).toBeInTheDocument();
|
||||
expect(screen.getByText(expectedDate)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+24
-5
@@ -8,14 +8,16 @@ import { BundleError } from './bundle-error';
|
||||
describe('Bundle Error', () => {
|
||||
const NON_ENABLED_STATUS: AssetStatus[] = [
|
||||
AssetStatus.STATUS_PENDING_LISTING,
|
||||
];
|
||||
|
||||
const NOT_SHOWN_STATUS: AssetStatus[] = [
|
||||
AssetStatus.STATUS_PROPOSED,
|
||||
AssetStatus.STATUS_REJECTED,
|
||||
];
|
||||
|
||||
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
|
||||
|
||||
it.each(NON_ENABLED_STATUS)(
|
||||
'shows the apollo error if not enabled and a message is provided',
|
||||
it.each(NOT_SHOWN_STATUS)(
|
||||
'does not render for proposed or rejected bundles',
|
||||
(status) => {
|
||||
const screen = render(
|
||||
<MemoryRouter>
|
||||
@@ -28,7 +30,24 @@ describe('Bundle Error', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText('test-error-message')).toBeInTheDocument();
|
||||
expect(screen.container).toBeEmptyDOMElement();
|
||||
}
|
||||
);
|
||||
it.each(NON_ENABLED_STATUS)(
|
||||
'shows the apollo error in a syntax highlighter if not enabled and a message is provided',
|
||||
(status) => {
|
||||
const screen = render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<BundleError
|
||||
error={{ message: 'test-error-message' } as ApolloError}
|
||||
status={status}
|
||||
/>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No signature bundle')).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -43,7 +62,7 @@ describe('Bundle Error', () => {
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText('No bundle for proposal ID')).toBeInTheDocument();
|
||||
expect(screen.getByText('No signature bundle')).toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -33,7 +33,7 @@ export const BundleError = ({ status, error }: BundleErrorProps) => {
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<div>
|
||||
{status === 'STATUS_ENABLED' ? (
|
||||
t('Asset already enabled')
|
||||
) : (
|
||||
@@ -43,7 +43,7 @@ export const BundleError = ({ status, error }: BundleErrorProps) => {
|
||||
<SyntaxHighlighter data={error} size="smaller" />
|
||||
</details>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -32,6 +32,7 @@ describe('Bundle Exists', () => {
|
||||
nonce={MOCK_NONCE}
|
||||
proposalId={MOCK_PROPOSAL_ID}
|
||||
signatures={MOCK_SIGNATURES}
|
||||
assetAddress={'0x123413423'}
|
||||
status={status}
|
||||
/>
|
||||
</MockedProvider>
|
||||
@@ -52,6 +53,7 @@ describe('Bundle Exists', () => {
|
||||
nonce={MOCK_NONCE}
|
||||
proposalId={MOCK_PROPOSAL_ID}
|
||||
signatures={MOCK_SIGNATURES}
|
||||
assetAddress={'0x123413423'}
|
||||
status={status}
|
||||
/>
|
||||
</MockedProvider>
|
||||
|
||||
+4
-14
@@ -1,6 +1,5 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { AssetStatus } from '@vegaprotocol/types';
|
||||
import { IconForBundleStatus } from './bundle-icon';
|
||||
import { getIcon } from './bundle-icon';
|
||||
|
||||
describe('Bundle status icon', () => {
|
||||
const NON_ENABLED_STATUS: AssetStatus[] = [
|
||||
@@ -15,30 +14,21 @@ describe('Bundle status icon', () => {
|
||||
it.each(NON_ENABLED_STATUS)(
|
||||
'show a sparkle icon if the bundle is unused',
|
||||
(status) => {
|
||||
const screen = render(<IconForBundleStatus status={status} />);
|
||||
const i = screen.getByRole('img');
|
||||
expect(i).toHaveAttribute('aria-label');
|
||||
expect(i.getAttribute('aria-label')).toMatch(/clean/);
|
||||
expect(getIcon(status)).toEqual('clean');
|
||||
}
|
||||
);
|
||||
|
||||
it.each(ERROR_STATUS)(
|
||||
'show an error icon if the bundle is unavailable',
|
||||
(status) => {
|
||||
const screen = render(<IconForBundleStatus status={status} />);
|
||||
const i = screen.getByRole('img');
|
||||
expect(i).toHaveAttribute('aria-label');
|
||||
expect(i.getAttribute('aria-label')).toMatch(/disable/);
|
||||
expect(getIcon(status)).toEqual('disable');
|
||||
}
|
||||
);
|
||||
|
||||
it.each(ENABLED_STATUS)(
|
||||
'shows a tick if the bundle is already used',
|
||||
(status) => {
|
||||
const screen = render(<IconForBundleStatus status={status} />);
|
||||
const i = screen.getByRole('img');
|
||||
expect(i).toHaveAttribute('aria-label');
|
||||
expect(i.getAttribute('aria-label')).toMatch(/tick-circle/);
|
||||
expect(getIcon(status)).toEqual('tick-circle');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
+9
-1
@@ -14,7 +14,14 @@ export interface IconForBundleStatusProps {
|
||||
export const IconForBundleStatus = ({ status }: IconForBundleStatusProps) => {
|
||||
const i = getIcon(status);
|
||||
|
||||
return <Icon className="float-left mt-2 mr-3" name={i} ariaLabel={status} />;
|
||||
return (
|
||||
<Icon
|
||||
className="float-left mt-2 mr-3"
|
||||
name={i}
|
||||
data-testid={i}
|
||||
ariaLabel={status}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function getIcon(status?: AssetStatus): IconName {
|
||||
@@ -22,6 +29,7 @@ export function getIcon(status?: AssetStatus): IconName {
|
||||
case 'STATUS_ENABLED':
|
||||
return 'tick-circle';
|
||||
case undefined:
|
||||
case 'STATUS_REJECTED':
|
||||
return 'disable';
|
||||
default:
|
||||
return 'clean';
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import type { EncodeListAssetParameters } from '../../../../../lib/encoders/abis/list-asset';
|
||||
import type { BridgeFunction } from './bundle-signers';
|
||||
import {
|
||||
getBridgeAddressFromNetworkParameter,
|
||||
getSigners,
|
||||
} from './bundle-signers';
|
||||
|
||||
describe('Bundle Signers helpers', () => {
|
||||
it('getBridgeAddressFromNetworkParameter handles invalid json', () => {
|
||||
expect(getBridgeAddressFromNetworkParameter('hi')).toEqual(null);
|
||||
expect(getBridgeAddressFromNetworkParameter('{hi]')).toEqual(null);
|
||||
expect(getBridgeAddressFromNetworkParameter('{"hi"}')).toEqual(null);
|
||||
expect(
|
||||
getBridgeAddressFromNetworkParameter(false as unknown as string)
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('getBridgeAddressFromNetworkParameter returns null if bridge adderss is not in expected place', () => {
|
||||
expect(
|
||||
getBridgeAddressFromNetworkParameter(`{
|
||||
"NetworkParamter": false
|
||||
}`)
|
||||
).toEqual(null);
|
||||
|
||||
expect(
|
||||
getBridgeAddressFromNetworkParameter(`{
|
||||
"network_id": "11155111",
|
||||
"chain_id": "11155111",
|
||||
"confirmations": 3,
|
||||
"staking_bridge_contract": {
|
||||
"address": "0xFFb0A0d4806502ceF491aF1141f66669A1Bd0D03",
|
||||
"deployment_block_height": 2011705
|
||||
},
|
||||
"token_vesting_contract": {
|
||||
"address": "0x680fF88252FA7071CAce7398e77872d54D781d0B",
|
||||
"deployment_block_height": 2011709
|
||||
},
|
||||
"multisig_control_contract": {
|
||||
"address": "0x6eBc32d66277D94DB8FF2ccF86E36f37F29a52D3",
|
||||
"deployment_block_height": 2011699
|
||||
}
|
||||
}`)
|
||||
).toEqual(null);
|
||||
});
|
||||
|
||||
it('getBridgeAddressFromNetworkParameter returns address if the collateral_bridge_contract has an address', () => {
|
||||
expect(
|
||||
getBridgeAddressFromNetworkParameter(`{
|
||||
"network_id": "11155111",
|
||||
"chain_id": "11155111",
|
||||
"confirmations": 3,
|
||||
"collateral_bridge_contract": {
|
||||
"address": "0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799"
|
||||
},
|
||||
"staking_bridge_contract": {
|
||||
"address": "0xFFb0A0d4806502ceF491aF1141f66669A1Bd0D03",
|
||||
"deployment_block_height": 2011705
|
||||
},
|
||||
"token_vesting_contract": {
|
||||
"address": "0x680fF88252FA7071CAce7398e77872d54D781d0B",
|
||||
"deployment_block_height": 2011709
|
||||
},
|
||||
"multisig_control_contract": {
|
||||
"address": "0x6eBc32d66277D94DB8FF2ccF86E36f37F29a52D3",
|
||||
"deployment_block_height": 2011699
|
||||
}
|
||||
}`)
|
||||
).toEqual('0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799');
|
||||
});
|
||||
|
||||
it('getSigners to return [] in the case of bad inputs', () => {
|
||||
expect(
|
||||
getSigners('list_asset', '123', '', {
|
||||
assetERC20: '123',
|
||||
assetId: '456',
|
||||
limit: 'bad',
|
||||
threshold: 'data',
|
||||
nonce: 'here',
|
||||
})
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
getSigners('nothing' as unknown as BridgeFunction, '123', '', {
|
||||
nonce: 'here',
|
||||
} as unknown as EncodeListAssetParameters)
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
getSigners('set_asset_limits', '0x123', '0x456', {
|
||||
nonce: 'here',
|
||||
} as unknown as EncodeListAssetParameters)
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
+66
-32
@@ -6,8 +6,13 @@ import { DApp, TOKEN_VALIDATOR, useLinks } from '@vegaprotocol/environment';
|
||||
import { ExternalLink, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import { encodeUpdateAssetBridgeTx } from '../../../../../lib/encoders/abis/update-asset';
|
||||
import { prepend0x } from '@vegaprotocol/smart-contracts';
|
||||
import type { EncodeListAssetParameters } from '../../../../../lib/encoders/abis/list-asset';
|
||||
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
export type BridgeFunction = 'list_asset' | 'set_asset_limits';
|
||||
|
||||
export interface BundleSignersProps {
|
||||
signatures: string;
|
||||
@@ -32,17 +37,13 @@ export const BundleSigners = ({
|
||||
}: BundleSignersProps) => {
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
|
||||
const bridgeFunction =
|
||||
const bridgeFunction: BridgeFunction =
|
||||
tx?.changes?.erc20 && 'contractAddress' in tx.changes.erc20
|
||||
? 'list_asset'
|
||||
: 'set_asset_limits';
|
||||
|
||||
const { data } = useExplorerBundleSignersQuery();
|
||||
|
||||
if (!id || !tx || !tx.changes?.erc20) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bridgeAddress = getBridgeAddressFromNetworkParameter(
|
||||
data?.networkParameter?.value
|
||||
);
|
||||
@@ -52,40 +53,34 @@ export const BundleSigners = ({
|
||||
?.filter((n) => n?.node.status === 'NODE_STATUS_VALIDATOR')
|
||||
.map((s) => s?.node) || [];
|
||||
|
||||
if (!tx || !tx.changes?.erc20) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { lifetimeLimit, withdrawThreshold } = tx.changes.erc20;
|
||||
|
||||
if (
|
||||
!id ||
|
||||
allEthereumKeys.length === 0 ||
|
||||
bridgeAddress === null ||
|
||||
!bridgeAddress ||
|
||||
!lifetimeLimit ||
|
||||
!withdrawThreshold
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const digest =
|
||||
bridgeFunction === 'list_asset'
|
||||
? encodeListAssetBridgeTx(
|
||||
{
|
||||
assetERC20: assetAddress,
|
||||
assetId: prepend0x(id),
|
||||
limit: lifetimeLimit,
|
||||
threshold: withdrawThreshold,
|
||||
nonce,
|
||||
},
|
||||
bridgeAddress
|
||||
)
|
||||
: encodeUpdateAssetBridgeTx(
|
||||
{
|
||||
assetERC20: assetAddress,
|
||||
limit: lifetimeLimit,
|
||||
threshold: withdrawThreshold,
|
||||
nonce,
|
||||
},
|
||||
bridgeAddress
|
||||
);
|
||||
|
||||
const signersLowerCase = recoverAddressesFromDigest(digest, signatures);
|
||||
const signersLowerCase = getSigners(
|
||||
bridgeFunction,
|
||||
bridgeAddress,
|
||||
signatures,
|
||||
{
|
||||
assetERC20: assetAddress,
|
||||
assetId: prepend0x(id),
|
||||
limit: lifetimeLimit,
|
||||
threshold: withdrawThreshold,
|
||||
nonce,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -122,6 +117,42 @@ export const BundleSigners = ({
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given all of the collated information, this function creates an equivalent unsigned bundle
|
||||
* and recovers the signers from it, In the case of an error, it returns an empty array.
|
||||
*
|
||||
* @param bridgeFunction Decides which data goes in to the digest
|
||||
* @param bridgeAddress ERC20 bridge address
|
||||
* @param signatures Long string of signatures
|
||||
* @param params The object containing all data that the bridge requires for New or Updating assets
|
||||
* @returns String[] Empty if there was an error or no signers were recovered, otherwise lowercased ETH addresses
|
||||
*/
|
||||
export function getSigners(
|
||||
bridgeFunction: BridgeFunction,
|
||||
bridgeAddress: string,
|
||||
signatures: string,
|
||||
params: EncodeListAssetParameters
|
||||
): string[] {
|
||||
try {
|
||||
if (bridgeFunction === 'list_asset') {
|
||||
const digest = encodeListAssetBridgeTx(params, bridgeAddress);
|
||||
|
||||
// Recover Address from digest can return null, which is handled as an empty array
|
||||
return recoverAddressesFromDigest(digest, signatures) || [];
|
||||
} else {
|
||||
// The params bundles are so similar, rather than force the component to make two different
|
||||
// styles, just delete the one different property
|
||||
const p = omit(params, 'assetId');
|
||||
const digest = encodeUpdateAssetBridgeTx(p, bridgeAddress);
|
||||
return recoverAddressesFromDigest(digest, signatures) || [];
|
||||
}
|
||||
} catch (e) {
|
||||
// In the worst case, no signing addresses are recovered. This means that all nodes will
|
||||
// be rendered as if they had not signed the bundle.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Querying for the network parameter value gets us all of the contract details for this network
|
||||
* encoded as a JSON object. This function pulls out the address for the bridge, or returns null
|
||||
@@ -130,7 +161,7 @@ export const BundleSigners = ({
|
||||
* @param networkParameterAsString the stringified JSON object
|
||||
* @returns null or bridge address as a string
|
||||
*/
|
||||
function getBridgeAddressFromNetworkParameter(
|
||||
export function getBridgeAddressFromNetworkParameter(
|
||||
networkParameterAsString: string | undefined
|
||||
): string | null {
|
||||
if (!networkParameterAsString) {
|
||||
@@ -146,7 +177,10 @@ function getBridgeAddressFromNetworkParameter(
|
||||
}
|
||||
}
|
||||
|
||||
function recoverAddressesFromDigest(digest: string, unprefixedBundle: string) {
|
||||
export function recoverAddressesFromDigest(
|
||||
digest: string,
|
||||
unprefixedBundle: string
|
||||
) {
|
||||
// Remove 0x from bundle, then split it in to signatures
|
||||
const sigs = unprefixedBundle.substring(2).match(/.{1,130}/g);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { keccak256, defaultAbiCoder } from 'ethers/lib/utils';
|
||||
import { keccak256, defaultAbiCoder, isAddress } from 'ethers/lib/utils';
|
||||
import type { AbiType } from './abi-types';
|
||||
|
||||
export const BRIDGE_COMMAND: AbiType[] = [
|
||||
@@ -14,14 +14,13 @@ export const BRIDGE_COMMAND: AbiType[] = [
|
||||
* @param bytes The packed bytes of the command for the bridge
|
||||
* @param address the Ethereum address of the ERC20 bridge
|
||||
* @param raw defaults to false. If set, does not keccak256 the output
|
||||
* @returns
|
||||
*/
|
||||
export function encodeBridgeCommand(
|
||||
bytes: string,
|
||||
address: string,
|
||||
raw = false
|
||||
) {
|
||||
if (address.substring(0, 2) !== '0x') {
|
||||
if (!isAddress(address)) {
|
||||
throw new Error('Bridge address must be a hex value');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { encodeBridgeCommand } from './bridge-command';
|
||||
|
||||
describe('Bridge command encoder', () => {
|
||||
const VALID_BYTES =
|
||||
'0x000000000000000000000000b063f5504610ba4b8db230d9f884bfadc1e31da00b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b3790000000000000000000000000000000000000000000000487a9a30453944000000000000000000000000000000000000000000000000000000000000000000010b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b37900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000a6c6973745f617373657400000000000000000000000000000000000000000000';
|
||||
const VALID_ADDRESS = '0x7fe27d970bc8Afc3B11Cc8d9737bfB66B1efd799';
|
||||
|
||||
it('rejects non valid bridge addresses', () => {
|
||||
expect(() => {
|
||||
encodeBridgeCommand(VALID_BYTES, '456789');
|
||||
}).toThrowError('Bridge address must be a hex value');
|
||||
});
|
||||
|
||||
it('throws if the bytes are not bytes-like', () => {
|
||||
expect(() => {
|
||||
encodeBridgeCommand('hello', VALID_ADDRESS);
|
||||
}).toThrowError(/invalid/);
|
||||
});
|
||||
|
||||
it('keccac256s the value by default', () => {
|
||||
const res = encodeBridgeCommand(VALID_BYTES, VALID_ADDRESS);
|
||||
// Magic number: Known output, including 0x
|
||||
expect(res.length).toEqual(66);
|
||||
});
|
||||
|
||||
it('Does not keccac256 the value if third param is set', () => {
|
||||
const res = encodeBridgeCommand(VALID_BYTES, VALID_ADDRESS, true);
|
||||
// Magic number: Known output
|
||||
expect(res.length).toEqual(706);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { encodeListAsset, encodeListAssetBridgeTx } from './list-asset';
|
||||
|
||||
describe('List Asset ABI encoder', () => {
|
||||
it('throws if asset erc20 address is invalid', () => {
|
||||
expect(() => {
|
||||
encodeListAsset({
|
||||
assetERC20: '123',
|
||||
assetId: '0x456',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
}).toThrowError('Asset ERC20 and assetID must be hex values');
|
||||
});
|
||||
|
||||
it('throws if assetId is not hex encoded', () => {
|
||||
expect(() => {
|
||||
encodeListAsset({
|
||||
assetERC20: '0x123',
|
||||
assetId: '456',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
}).toThrowError('Asset ERC20 and assetID must be hex values');
|
||||
});
|
||||
|
||||
it('throws if values to not match expected format', () => {
|
||||
expect(() => {
|
||||
encodeListAsset({
|
||||
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
assetId: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
limit: 'not a valid number',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
}).toThrowError(/incorrect data length/);
|
||||
});
|
||||
|
||||
it('returns an ABI encoded value if inputs are valid', () => {
|
||||
const EXPECTED_OUTPUT =
|
||||
'0x000000000000000000000000b063f5504610ba4b8db230d9f884bfadc1e31da00b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b37900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000a6c6973745f617373657400000000000000000000000000000000000000000000';
|
||||
|
||||
const res = encodeListAsset({
|
||||
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
assetId:
|
||||
'0x0b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b379',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
|
||||
expect(res).toEqual(EXPECTED_OUTPUT);
|
||||
});
|
||||
|
||||
it('encodeListAssetBridge returns a keccak256 hash of the bridge tx', () => {
|
||||
const EXPECTED_OUTPUT =
|
||||
'0xe0e62b27fe4490025d312bb2e37486f56935a3d9442dc34c2b918b2a28a386f2';
|
||||
|
||||
const res = encodeListAssetBridgeTx(
|
||||
{
|
||||
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
assetId:
|
||||
'0x0b87ac58d4af7fc11c8b417153fcb62631cfd9643835ef28db3f5a1caef0b379',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
},
|
||||
'0xb063f5504610ba4b8db230d9f884bfadc1e31da0'
|
||||
);
|
||||
|
||||
// Magic number: keccak256 hash length + '0x'
|
||||
expect(res.length).toEqual(66);
|
||||
expect(res).toEqual(EXPECTED_OUTPUT);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultAbiCoder } from 'ethers/lib/utils';
|
||||
import { defaultAbiCoder, isAddress, isHexString } from 'ethers/lib/utils';
|
||||
import { encodeBridgeCommand } from './bridge-command';
|
||||
import type { AbiType } from './abi-types';
|
||||
|
||||
@@ -33,7 +33,13 @@ export interface EncodeListAssetParameters {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an ABI encoded function call to list an asset
|
||||
* Generates an ABI encoded function call to list an asset. This is
|
||||
* used in the Signature Bundle view on some proposals to recover
|
||||
* which validators signed a multisig bundle. It does this by recovering
|
||||
* the ERC20 addresses of the signers, then comparing those to the list
|
||||
* of signers on the bundle. In order to do this, we recreate the signed
|
||||
* data from the values we know from the transaction. That last part
|
||||
* is what this function does.
|
||||
*
|
||||
* @param EncodeListAssetParameters The arguments for the ABI call
|
||||
* @returns string encoded message
|
||||
@@ -45,7 +51,7 @@ export function encodeListAsset({
|
||||
threshold,
|
||||
nonce,
|
||||
}: EncodeListAssetParameters) {
|
||||
if (assetERC20.substring(0, 2) !== '0x' || assetId.substring(0, 2) !== '0x') {
|
||||
if (!isAddress(assetERC20) || !isHexString(assetId)) {
|
||||
throw new Error('Asset ERC20 and assetID must be hex values');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { encodeUpdateAsset, encodeUpdateAssetBridgeTx } from './update-asset';
|
||||
|
||||
describe('Update Asset ABI encoder', () => {
|
||||
it('throws if asset erc20 address is invalid', () => {
|
||||
expect(() => {
|
||||
encodeUpdateAsset({
|
||||
assetERC20: '123',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
}).toThrowError('Asset ERC20 must be a valid address');
|
||||
});
|
||||
|
||||
it('throws if an input is invalid', () => {
|
||||
expect(() => {
|
||||
encodeUpdateAsset({
|
||||
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
limit: 'hello',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
}).toThrowError(/invalid BigNumber/);
|
||||
});
|
||||
|
||||
it('returns an ABI encoded value if inputs are valid', () => {
|
||||
const EXPECTED_OUTPUT =
|
||||
'0x000000000000000000000000b063f5504610ba4b8db230d9f884bfadc1e31da000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000107365745f61737365745f6c696d69747300000000000000000000000000000000';
|
||||
|
||||
const res = encodeUpdateAsset({
|
||||
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
});
|
||||
|
||||
expect(res).toEqual(EXPECTED_OUTPUT);
|
||||
});
|
||||
|
||||
it('encodeUpdateAssetBridge returns a keccak256 hash of the bridge tx', () => {
|
||||
const EXPECTED_OUTPUT =
|
||||
'0xeb240131c4558aebfab3da0ddbea1ac0447b9f5670899af2d78795867631d877';
|
||||
|
||||
const res = encodeUpdateAssetBridgeTx(
|
||||
{
|
||||
assetERC20: '0xb063f5504610ba4b8db230d9f884bfadc1e31da0',
|
||||
limit: '1',
|
||||
threshold: '1',
|
||||
nonce: '1',
|
||||
},
|
||||
'0xb063f5504610ba4b8db230d9f884bfadc1e31da0'
|
||||
);
|
||||
|
||||
// Magic number: keccak256 hash length + '0x'
|
||||
expect(res.length).toEqual(66);
|
||||
expect(res).toEqual(EXPECTED_OUTPUT);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultAbiCoder } from 'ethers/lib/utils';
|
||||
import { defaultAbiCoder, isAddress } from 'ethers/lib/utils';
|
||||
import { encodeBridgeCommand } from './bridge-command';
|
||||
import type { AbiType } from './abi-types';
|
||||
|
||||
@@ -40,8 +40,8 @@ export function encodeUpdateAsset({
|
||||
threshold,
|
||||
nonce,
|
||||
}: EncodeUpdateAssetParameters) {
|
||||
if (assetERC20.substring(0, 2) !== '0x') {
|
||||
throw new Error('Asset ERC20 must be hex values');
|
||||
if (!isAddress(assetERC20)) {
|
||||
throw new Error('Asset ERC20 must be a valid address');
|
||||
}
|
||||
|
||||
const values = [assetERC20, limit, threshold, nonce, METHOD_NAME];
|
||||
|
||||
Reference in New Issue
Block a user