From d9d4f86fcb995e0563cda5d259c837baa9e1849d Mon Sep 17 00:00:00 2001
From: Edd
Date: Wed, 8 Mar 2023 12:01:55 +0000
Subject: [PATCH] chore(explorer): test abi encode functions
---
apps/explorer/src/app/components/nav/nav.tsx | 180 ------------------
.../chain-events/tx-multisig-signer.spec.tsx | 7 -
.../tx-multisig-threshold.spec.tsx | 4 -
.../signature-bundle/bundle-error.spec.tsx | 29 ++-
.../signature-bundle/bundle-error.tsx | 4 +-
.../signature-bundle/bundle-exists.spec.tsx | 2 +
.../signature-bundle/bundle-icon.spec.tsx | 18 +-
.../proposal/signature-bundle/bundle-icon.tsx | 10 +-
.../signature-bundle/bundle-signers.spec.tsx | 94 +++++++++
.../signature-bundle/bundle-signers.tsx | 98 ++++++----
.../app/lib/encoders/abis/bridge-command.ts | 5 +-
.../lib/encoders/abis/bridge-commands.spec.ts | 31 +++
.../app/lib/encoders/abis/list-asset.spec.ts | 76 ++++++++
.../src/app/lib/encoders/abis/list-asset.ts | 12 +-
.../lib/encoders/abis/update-asset.spec.ts | 58 ++++++
.../src/app/lib/encoders/abis/update-asset.ts | 6 +-
16 files changed, 380 insertions(+), 254 deletions(-)
delete mode 100644 apps/explorer/src/app/components/nav/nav.tsx
create mode 100644 apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.spec.tsx
create mode 100644 apps/explorer/src/app/lib/encoders/abis/bridge-commands.spec.ts
create mode 100644 apps/explorer/src/app/lib/encoders/abis/list-asset.spec.ts
create mode 100644 apps/explorer/src/app/lib/encoders/abis/update-asset.spec.ts
diff --git a/apps/explorer/src/app/components/nav/nav.tsx b/apps/explorer/src/app/components/nav/nav.tsx
deleted file mode 100644
index 3cf5129c9..000000000
--- a/apps/explorer/src/app/components/nav/nav.tsx
+++ /dev/null
@@ -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((set, get) => ({
- open: false,
- toggle: () => set({ open: !get().open }),
- hide: () => set({ open: false }),
-}));
-
-const NavLinks = ({ links }: { links: Navigable[] }) => {
- const navLinks = links.map((r) => (
-
-
- 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}
-
-
- ));
-
- return ;
-};
-
-export const Nav = () => {
- const [open, hide] = useNavStore((state) => [state.open, state.hide]);
- const location = useLocation();
-
- const navRef = useRef(null);
- const btnRef = useRef(null);
-
- const focusable = useMemo(
- () =>
- navRef.current
- ? [
- ...(navRef.current.querySelectorAll(
- 'a, button'
- ) as NodeListOf),
- ]
- : [],
- // 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 (
-
- );
-};
diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-signer.spec.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-signer.spec.tsx
index 23dca02c0..8dd1b595c 100644
--- a/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-signer.spec.tsx
+++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-signer.spec.tsx
@@ -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();
});
});
diff --git a/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-threshold.spec.tsx b/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-threshold.spec.tsx
index df51ffee5..de7f31e53 100644
--- a/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-threshold.spec.tsx
+++ b/apps/explorer/src/app/components/txs/details/chain-events/tx-multisig-threshold.spec.tsx
@@ -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();
});
});
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.spec.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.spec.tsx
index b2140c6d5..d4c40e70a 100644
--- a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.spec.tsx
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.spec.tsx
@@ -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(
@@ -28,7 +30,24 @@ describe('Bundle Error', () => {
);
- 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(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('No signature bundle')).toBeInTheDocument();
}
);
@@ -43,7 +62,7 @@ describe('Bundle Error', () => {
);
- expect(screen.getByText('No bundle for proposal ID')).toBeInTheDocument();
+ expect(screen.getByText('No signature bundle')).toBeInTheDocument();
}
);
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.tsx
index 09fbb4fd3..c4edc894e 100644
--- a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.tsx
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-error.tsx
@@ -33,7 +33,7 @@ export const BundleError = ({ status, error }: BundleErrorProps) => {
)}
-
+
{status === 'STATUS_ENABLED' ? (
t('Asset already enabled')
) : (
@@ -43,7 +43,7 @@ export const BundleError = ({ status, error }: BundleErrorProps) => {
)}
-
+
);
};
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-exists.spec.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-exists.spec.tsx
index ce4c7bd99..faeed13a8 100644
--- a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-exists.spec.tsx
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-exists.spec.tsx
@@ -32,6 +32,7 @@ describe('Bundle Exists', () => {
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
+ assetAddress={'0x123413423'}
status={status}
/>
@@ -52,6 +53,7 @@ describe('Bundle Exists', () => {
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
+ assetAddress={'0x123413423'}
status={status}
/>
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.spec.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.spec.tsx
index 6f2215057..01fa8a20c 100644
--- a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.spec.tsx
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.spec.tsx
@@ -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();
- 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();
- 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();
- const i = screen.getByRole('img');
- expect(i).toHaveAttribute('aria-label');
- expect(i.getAttribute('aria-label')).toMatch(/tick-circle/);
+ expect(getIcon(status)).toEqual('tick-circle');
}
);
});
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.tsx
index c5c435f2d..2f68dd15c 100644
--- a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.tsx
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-icon.tsx
@@ -14,7 +14,14 @@ export interface IconForBundleStatusProps {
export const IconForBundleStatus = ({ status }: IconForBundleStatusProps) => {
const i = getIcon(status);
- return ;
+ return (
+
+ );
};
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';
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.spec.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.spec.tsx
new file mode 100644
index 000000000..0750bc6e6
--- /dev/null
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.spec.tsx
@@ -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([]);
+ });
+});
diff --git a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.tsx b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.tsx
index 3936dadc7..154a717e8 100644
--- a/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.tsx
+++ b/apps/explorer/src/app/components/txs/details/proposal/signature-bundle/bundle-signers.tsx
@@ -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);
diff --git a/apps/explorer/src/app/lib/encoders/abis/bridge-command.ts b/apps/explorer/src/app/lib/encoders/abis/bridge-command.ts
index ad2631e0e..8cd5dab24 100644
--- a/apps/explorer/src/app/lib/encoders/abis/bridge-command.ts
+++ b/apps/explorer/src/app/lib/encoders/abis/bridge-command.ts
@@ -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');
}
diff --git a/apps/explorer/src/app/lib/encoders/abis/bridge-commands.spec.ts b/apps/explorer/src/app/lib/encoders/abis/bridge-commands.spec.ts
new file mode 100644
index 000000000..436ad8e9e
--- /dev/null
+++ b/apps/explorer/src/app/lib/encoders/abis/bridge-commands.spec.ts
@@ -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);
+ });
+});
diff --git a/apps/explorer/src/app/lib/encoders/abis/list-asset.spec.ts b/apps/explorer/src/app/lib/encoders/abis/list-asset.spec.ts
new file mode 100644
index 000000000..8b64f8009
--- /dev/null
+++ b/apps/explorer/src/app/lib/encoders/abis/list-asset.spec.ts
@@ -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);
+ });
+});
diff --git a/apps/explorer/src/app/lib/encoders/abis/list-asset.ts b/apps/explorer/src/app/lib/encoders/abis/list-asset.ts
index dc954f4b6..c40f003cb 100644
--- a/apps/explorer/src/app/lib/encoders/abis/list-asset.ts
+++ b/apps/explorer/src/app/lib/encoders/abis/list-asset.ts
@@ -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');
}
diff --git a/apps/explorer/src/app/lib/encoders/abis/update-asset.spec.ts b/apps/explorer/src/app/lib/encoders/abis/update-asset.spec.ts
new file mode 100644
index 000000000..5e1485104
--- /dev/null
+++ b/apps/explorer/src/app/lib/encoders/abis/update-asset.spec.ts
@@ -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);
+ });
+});
diff --git a/apps/explorer/src/app/lib/encoders/abis/update-asset.ts b/apps/explorer/src/app/lib/encoders/abis/update-asset.ts
index 7884c25c3..728fee6e6 100644
--- a/apps/explorer/src/app/lib/encoders/abis/update-asset.ts
+++ b/apps/explorer/src/app/lib/encoders/abis/update-asset.ts
@@ -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];