test: allow connecting to a local ganache node by signing transactions in browser (#595)

* test: allow connecting to a local ganache node by signing transactions in brower

* chore: update environment variables

* fix: disassocaition

* chore: remove redundant code

* chore: rewrite promise as async/await

* fix: approval amount too low

* chore: address PR comments

* test: fix tets

* revert changes to env file

* chore: changes as per pr comments
This commit is contained in:
Dexter Edwards 2022-06-24 10:07:05 +01:00 committed by GitHub
parent b4c1972ea5
commit 994bb4cf2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 258 additions and 103 deletions

View File

@ -1,29 +1,17 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
REACT_APP_VERSION=$npm_package_version
REACT_APP_REPOSITORY_URL=$REPOSITORY_URL
REACT_APP_BRANCH=$BRANCH
REACT_APP_PULL_REQUEST=$PULL_REQUEST
REACT_APP_HEAD=$HEAD
REACT_APP_COMMIT_REF=$COMMIT_REF
REACT_APP_CONTEXT=$CONTEXT
REACT_APP_REVIEW_ID=$REVIEW_ID
REACT_APP_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
REACT_APP_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
REACT_APP_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
REACT_APP_URL=$URL
REACT_APP_DEPLOY_URL=$DEPLOY_URL
REACT_APP_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://lb.testnet.vega.xyz/query
NX_ETHEREUM_PROVIDER_URL=https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHEREUM_PROVIDER_URL=http://localhost:8545
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_FAIRGROUND=false
NX_IS_NEW_BRIDGE_CONTRACT=true
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET":"https://dev.token.vega.xyz","STAGNET2":"staging2.token.vega.xyz","TESTNET":"token.fairground.wtf","MAINNET":"token.vega.xyz"}'
NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_REST=http://localhost:3029
NX_ETHEREUM_CHAIN_ID=1440
NX_ETH_URL_CONNECT=1
NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
#Test configuration variables
CYPRESS_FAIRGROUND=false

View File

@ -1,23 +1,3 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
REACT_APP_VERSION=$npm_package_version
REACT_APP_REPOSITORY_URL=$REPOSITORY_URL
REACT_APP_BRANCH=$BRANCH
REACT_APP_PULL_REQUEST=$PULL_REQUEST
REACT_APP_HEAD=$HEAD
REACT_APP_COMMIT_REF=$COMMIT_REF
REACT_APP_CONTEXT=$CONTEXT
REACT_APP_REVIEW_ID=$REVIEW_ID
REACT_APP_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
REACT_APP_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
REACT_APP_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
REACT_APP_URL=$URL
REACT_APP_DEPLOY_URL=$DEPLOY_URL
REACT_APP_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_VEGA_ENV=TESTNET
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json

17
apps/token/.env.capsule Normal file
View File

@ -0,0 +1,17 @@
# App configuration variables
NX_VEGA_ENV=TESTNET
NX_ETHEREUM_PROVIDER_URL=http://localhost:8545
NX_ETHERSCAN_URL=https://ropsten.etherscan.io
NX_FAIRGROUND=false
NX_IS_NEW_BRIDGE_CONTRACT=true
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET":"https://dev.token.vega.xyz","STAGNET2":"staging2.token.vega.xyz","TESTNET":"token.fairground.wtf","MAINNET":"token.vega.xyz"}'
NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_REST=http://localhost:3029
NX_ETHEREUM_CHAIN_ID=1440
NX_ETH_URL_CONNECT=1
NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
NX_LOCAL_PROVIDER_URL=http://localhost:8545/
#Test configuration variables
CYPRESS_FAIRGROUND=false

View File

@ -38,23 +38,19 @@ yarn nx run token:serve --env={env} # e.g. stagnet1
There are a few different configuration options offered for this app:
| **Flag** | **Purpose** |
| ------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `NX_APP_SENTRY_DSN` | The sentry endpoint to report to. Should be off in dev but set in live. |
| `NX_APP_CHAIN` | The ETH chain for the app to work on. Should be mainnet for live, but ropsten for preview deploys. |
| `NX_APP_VEGA_URL` | The GraphQL query endpoint of a [Vega data node](https://github.com/vegaprotocol/networks#data-node) |
| `NX_APP_DEX_STAKING_DISABLED` | Disable the dex liquidity page an show a coming soon message |
| `NX_APP_FAIRGROUND` | Change styling to be themed as the fairground version of the website |
| `NX_APP_INFURA_ID` | Infura fallback for if the user does not have a web3 compatible browser |
| `NX_APP_HOSTED_WALLET_ENABLED` | If the hosted wallet is enabled or not. If so then allow users to login using the hosted wallet |
| `NX_APP_ENV` | Change network to connect to. When set to CUSTOM use CUSTOM\_\* vars for network parameters |
| `NX_CUSTOM_URLS` | When NX_APP_ENV=CUSTOM use these Data Node REST URLs, optional if CUSTOM_URLS_WITH_GRAPHQL is used. |
| `NX_CUSTOM_URLS_WITH_GRAPHQL` | When NX_APP_ENV=CUSTOM use these Data Node GraphQL URLs, optional if CUSTOM_URLS is used. |
| `NX_CUSTOM_TOKEN_ADDRESS` | When NX_APP_ENV=CUSTOM specify Vega token address. |
| `NX_CUSTOM_CLAIM_ADDRESS` | When NX_APP_ENV=CUSTOM specify Vega claim address. |
| `NX_CUSTOM_LOCKED_ADDRESS` | When NX_APP_ENV=CUSTOM specify Vega locked address. |
| `NX_CUSTOM_VESTING_ADDRESS` | When NX_APP_ENV=CUSTOM specify Vega vesting address. |
| `NX_CUSTOM_STAKING_BRIDGE` | When NX_APP_ENV=CUSTOM specify Vega staking bridge address. |
| **Flag** | **Purpose** |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NX_APP_SENTRY_DSN` | The sentry endpoint to report to. Should be off in dev but set in live. |
| `NX_APP_CHAIN` | The ETH chain for the app to work on. Should be mainnet for live, but ropsten for preview deploys. |
| `NX_APP_VEGA_URL` | The GraphQL query endpoint of a [Vega data node](https://github.com/vegaprotocol/networks#data-node) |
| `NX_APP_DEX_STAKING_DISABLED` | Disable the dex liquidity page an show a coming soon message |
| `NX_APP_FAIRGROUND` | Change styling to be themed as the fairground version of the website |
| `NX_APP_INFURA_ID` | Infura fallback for if the user does not have a web3 compatible browser |
| `NX_APP_HOSTED_WALLET_ENABLED` | If the hosted wallet is enabled or not. If so then allow users to login using the hosted wallet |
| `NX_APP_ENV` | Change network to connect to. When set to CUSTOM use CUSTOM\_\* vars for network parameters |
| `NX_ETH_URL_CONNECT` (optional) | If set to true the below two must also be set. This allows siging transactions in brower to allow to connect to a local ganache node through cypress |
| `NX_ETH_WALLET_MNEMONIC` (optional) | The mnemonic to be used to sign transactions with in browser |
| `NX_LOCAL_PROVIDER_URL` (optional) | The local node to use to send transaction to when signing in browser |
## Example configs:

View File

@ -55,6 +55,9 @@ export const ENV = {
commit: windowOrDefault('NX_COMMIT_REF'),
branch: windowOrDefault('NX_BRANCH'),
vegaUrl: windowOrDefault('NX_VEGA_URL'),
urlConnect: TRUTHY.includes(windowOrDefault('NX_ETH_URL_CONNECT')),
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
flags: {
NETWORK_DOWN: TRUTHY.includes(windowOrDefault('NX_NETWORK_DOWN')),
HOSTED_WALLET_ENABLED: TRUTHY.includes(

View File

@ -1,10 +1,8 @@
import React from 'react';
import { useContracts } from '../../contexts/contracts/contracts-context';
import mock from './tranches-mock';
import type { Tranche } from '@vegaprotocol/smart-contracts';
export function useTranches() {
const { vesting } = useContracts();
const [tranches, setTranches] = React.useState<Tranche[] | null>(null);
const [error, setError] = React.useState<string | null>(null);
@ -17,7 +15,7 @@ export function useTranches() {
}
};
run();
}, [vesting]);
}, []);
return { tranches, error };
}

View File

@ -1,3 +1,4 @@
import { toBigNum } from '@vegaprotocol/react-helpers';
import React from 'react';
import {
@ -7,23 +8,26 @@ import {
import { useContracts } from '../contexts/contracts/contracts-context';
export function useRefreshAssociatedBalances() {
const { appDispatch } = useAppState();
const {
appDispatch,
appState: { decimals },
} = useAppState();
const { staking, vesting } = useContracts();
return React.useCallback(
async (ethAddress: string, vegaKey: string) => {
const [walletAssociatedBalance, vestingAssociatedBalance] =
await Promise.all([
staking.stakeBalance(ethAddress, `0x${vegaKey}`),
vesting.stakeBalance(ethAddress, `0x${vegaKey}`),
staking.stakeBalance(ethAddress, vegaKey),
vesting.stakeBalance(ethAddress, vegaKey),
]);
appDispatch({
type: AppStateActionType.REFRESH_ASSOCIATED_BALANCES,
walletAssociatedBalance,
vestingAssociatedBalance,
walletAssociatedBalance: toBigNum(walletAssociatedBalance, decimals),
vestingAssociatedBalance: toBigNum(vestingAssociatedBalance, decimals),
});
},
[staking, vesting, appDispatch]
[staking, vesting, appDispatch, decimals]
);
}

View File

@ -0,0 +1,150 @@
import { ethers, Wallet } from 'ethers';
import { Connector } from '@web3-react/types';
import { Eip1193Bridge } from '@ethersproject/experimental';
import type { ConnectionInfo } from '@ethersproject/web';
import type { Actions } from '@web3-react/types';
import { ENV } from '../config/env';
export class CustomizedBridge extends Eip1193Bridge {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async sendAsync(...args: any) {
console.debug('sendAsync called', ...args);
return this.send(...args);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
override async send(...args: any) {
console.debug('send called', ...args);
const isCallbackForm =
typeof args[0] === 'object' && typeof args[1] === 'function';
let callback;
let method;
let params;
if (isCallbackForm) {
callback = args[1];
method = args[0].method;
params = args[0].params;
} else {
method = args[0];
params = args[1];
}
try {
// Hacky, https://github.com/ethers-io/ethers.js/issues/1683#issuecomment-1016227588
// If from is present on eth_call it errors, removing it makes the library set
// from as the connected wallet which works fine
if (params && params.length && params[0].from && method === 'eth_call')
delete params[0].from;
let result;
// For sending a transaction if we call send it will error
// as it wants gasLimit in sendTransaction but hexlify sets the property gas
// to gasLimit which makes sensd transaction error.
// This has taken the code from the super method for sendTransaction and altered
// it slightly to make it work with the gas limit issues.
if (
params &&
params.length &&
params[0].from &&
method === 'eth_sendTransaction'
) {
// Hexlify will not take gas, must be gasLimit, set this property to be gasLimit
params[0].gasLimit = params[0].gas;
delete params[0].gas;
// If from is present on eth_sendTransaction it errors, removing it makes the library set
// from as the connected wallet which works fine
delete params[0].from;
const req = ethers.providers.JsonRpcProvider.hexlifyTransaction(
params[0]
);
// Hexlify sets the gasLimit property to be gas again and send transaction requires gasLimit
req['gasLimit'] = req['gas'];
delete req['gas'];
if (!this.signer) {
throw new Error('No signer');
}
// Send the transaction
const tx = await this.signer.sendTransaction(req);
result = tx.hash;
} else {
// All other transactions the base class works for
result = await super.send(method, params);
}
console.debug('result received', method, params, result);
if (isCallbackForm) {
callback(null, { result });
} else {
return result;
}
} catch (error) {
console.error(error);
if (isCallbackForm) {
callback(error, null);
} else {
throw error;
}
}
}
}
type url = string | ConnectionInfo;
export class Url extends Connector {
/** {@inheritdoc Connector.provider} */
public override provider: Eip1193Bridge | undefined;
private eagerConnection?: Promise<void>;
private url: url;
/**
* @param url - An RPC url.
* @param connectEagerly - A flag indicating whether connection should be initiated when the class is constructed.
*/
constructor(actions: Actions, url: url, connectEagerly = false) {
super(actions);
if (connectEagerly && typeof window === 'undefined') {
throw new Error(
'connectEagerly = true is invalid for SSR, instead use the activate method in a useEffect'
);
}
this.url = url;
if (connectEagerly) void this.activate();
}
private async isomorphicInitialize() {
if (this.eagerConnection) return this.eagerConnection;
await (this.eagerConnection = import('@ethersproject/providers')
.then(({ JsonRpcProvider }) => JsonRpcProvider)
.then((JsonRpcProvider) => {
const provider = new JsonRpcProvider(this.url);
const privateKey = Wallet.fromMnemonic(
ENV.ethWalletMnemonic,
`m/44'/60'/0'/0/0`
).privateKey;
const signer = new Wallet(privateKey, provider);
this.provider = new CustomizedBridge(signer, provider);
this.actions.update({ accounts: [signer.address], chainId: 1440 });
}));
}
/** {@inheritdoc Connector.activate} */
public async activate(): Promise<void> {
this.actions.startActivation();
await this.isomorphicInitialize();
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const chainId = await this.provider!.request({ method: 'eth_chainId' });
this.actions.update({ chainId: Number(chainId) });
} catch (error) {
this.actions.reportError(error as Error);
}
}
}

View File

@ -3,11 +3,18 @@ import type { Web3ReactHooks } from '@web3-react/core';
import { initializeConnector } from '@web3-react/core';
import { MetaMask } from '@web3-react/metamask';
import { WalletConnect } from '@web3-react/walletconnect';
import { Url } from './url-connector';
import type { Connector } from '@web3-react/types';
import { ENV } from '../config/env';
const [metamask, metamaskHooks] = initializeConnector<MetaMask>(
(actions) => new MetaMask(actions)
);
const [urlConnector, urlHooks] = initializeConnector<Url>(
(actions) => new Url(actions, ENV.localProviderUrl)
);
export const createDefaultProvider = (providerUrl: string, chainId: number) => {
return new ethers.providers.JsonRpcProvider(providerUrl, chainId);
};
@ -27,7 +34,8 @@ export const createConnectors = (providerUrl: string, chainId: number) => {
[chainId]
);
return [
ENV.urlConnect ? [urlConnector, urlHooks] : null,
[metamask, metamaskHooks],
[walletconnect, walletconnectHooks],
] as [MetaMask | WalletConnect, Web3ReactHooks][];
].filter(Boolean) as [Connector, Web3ReactHooks][];
};

View File

@ -43,7 +43,7 @@ const Contracts = () => {
title={t('View address on Etherscan')}
href={`${ETHERSCAN_URL}/address/${contract.address}`}
>
{config.collateral_bridge_contract.address}
{contract.address}
</Link>
</div>
);
@ -58,7 +58,6 @@ const Contracts = () => {
title={t('View address on Etherscan')}
href={`${ETHERSCAN_URL}/address/${value}`}
>
asdfasd
{value}
</Link>
</div>

View File

@ -8,7 +8,6 @@ import { Link } from 'react-router-dom';
import { EthConnectPrompt } from '../../components/eth-connect-prompt';
import { SplashLoader } from '../../components/splash-loader';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { useContracts } from '../../contexts/contracts/contracts-context';
import { useTranches } from '../../hooks/use-tranches';
import { Routes as RoutesConfig } from '../router-config';
import {
@ -19,7 +18,6 @@ import {
const RedemptionRouter = () => {
const { t } = useTranslation();
const { vesting } = useContracts();
const [state, dispatch] = React.useReducer(
redemptionReducer,
initialRedemptionState
@ -49,7 +47,7 @@ const RedemptionRouter = () => {
if (account) {
run(account);
}
}, [account, tranches, vesting]);
}, [account, tranches]);
if (error) {
return (

View File

@ -12,7 +12,7 @@ import { useTransaction } from '../../../hooks/use-transaction';
import { BigNumber } from '../../../lib/bignumber';
import { AssociateInfo } from './associate-info';
import type { VegaKeyExtended } from '@vegaprotocol/wallet';
import { toBigNum } from '@vegaprotocol/react-helpers';
import { removeDecimal, toBigNum } from '@vegaprotocol/react-helpers';
import type { EthereumConfig } from '@vegaprotocol/web3';
export const WalletAssociate = ({
@ -35,19 +35,18 @@ export const WalletAssociate = ({
appDispatch,
appState: { walletBalance, allowance, walletAssociatedBalance, decimals },
} = useAppState();
const { token } = useContracts();
const {
state: approveState,
perform: approve,
dispatch: approveDispatch,
} = useTransaction(() =>
token.approve(
} = useTransaction(() => {
return token.approve(
ethereumConfig.staking_bridge_contract.address,
Number.MAX_SAFE_INTEGER.toString()
)
);
removeDecimal('1000000', decimals).toString()
);
});
// Once they have approved deposits then we need to refresh their allowance
React.useEffect(() => {

View File

@ -37,7 +37,6 @@ beforeEach(() => {
submitDeposit: jest.fn(),
requestFaucet: jest.fn(),
limits: {
min: new BigNumber(0),
max: new BigNumber(20),
},
allowance: new BigNumber(30),
@ -199,6 +198,6 @@ it('Deposit', async () => {
// @ts-ignore contract address definitely defined
assetSource: asset.source.contractAddress,
amount: '1500',
vegaPublicKey: `0x${vegaKey}`,
vegaPublicKey: vegaKey,
});
});

View File

@ -89,7 +89,7 @@ export const DepositForm = ({
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: removeDecimal(fields.amount, selectedAsset.decimals),
vegaPublicKey: `0x${fields.to}`,
vegaPublicKey: fields.to,
});
};

View File

@ -1,6 +1,7 @@
import type { BigNumber } from 'ethers';
import { ethers } from 'ethers';
import abi from '../abis/erc20_bridge_new_abi.json';
import { prepend0x } from '../utils';
export class CollateralBridgeNew {
public contract: ethers.Contract;
@ -14,7 +15,11 @@ export class CollateralBridgeNew {
}
depositAsset(assetSource: string, amount: string, vegaPublicKey: string) {
return this.contract.deposit_asset(assetSource, amount, vegaPublicKey);
return this.contract.deposit_asset(
assetSource,
amount,
prepend0x(vegaPublicKey)
);
}
getAssetSource(vegaAssetId: string) {
return this.contract.get_asset_source(vegaAssetId);

View File

@ -1,6 +1,7 @@
import type { BigNumber } from 'ethers';
import { ethers } from 'ethers';
import abi from '../abis/erc20_bridge_abi.json';
import { prepend0x } from '../utils';
export class CollateralBridge {
public contract: ethers.Contract;
@ -16,7 +17,11 @@ export class CollateralBridge {
}
depositAsset(assetSource: string, amount: string, vegaPublicKey: string) {
return this.contract.deposit_asset(assetSource, amount, vegaPublicKey);
return this.contract.deposit_asset(
assetSource,
amount,
prepend0x(vegaPublicKey)
);
}
getAssetSource(vegaAssetId: string) {
return this.contract.get_asset_source(vegaAssetId);

View File

@ -1,5 +1,6 @@
import { ethers } from 'ethers';
import abi from '../abis/staking_abi.json';
import { prepend0x } from '../utils';
export class StakingBridge {
public contract: ethers.Contract;
@ -14,19 +15,23 @@ export class StakingBridge {
}
stake(amount: string, vegaPublicKey: string) {
return this.contract.stake(amount, `0x${vegaPublicKey}`);
return this.contract.stake(amount, prepend0x(vegaPublicKey));
}
removeStake(amount: string, vegaPublicKey: string) {
return this.contract.remove_stake(amount, `0x${vegaPublicKey}`);
return this.contract.remove_stake(amount, prepend0x(vegaPublicKey));
}
transferStake(amount: string, newAddress: string, vegaPublicKey: string) {
return this.contract.transfer_stake(amount, newAddress, vegaPublicKey);
return this.contract.transfer_stake(
amount,
newAddress,
prepend0x(vegaPublicKey)
);
}
stakingToken() {
return this.contract.staking_token();
}
stakeBalance(target: string, vegaPublicKey: string) {
return this.contract.stake_balance(target, vegaPublicKey);
return this.contract.stake_balance(target, prepend0x(vegaPublicKey));
}
totalStaked() {
return this.contract.total_staked();

View File

@ -1,5 +1,6 @@
import { ethers } from 'ethers';
import abi from '../abis/vesting_abi.json';
import { prepend0x } from '../utils';
export class TokenVesting {
public contract: ethers.Contract;
@ -14,13 +15,13 @@ export class TokenVesting {
}
stakeTokens(amount: string, vegaPublicKey: string) {
return this.contract.stake_tokens(amount, vegaPublicKey);
return this.contract.stake_tokens(amount, prepend0x(vegaPublicKey));
}
removeStake(amount: string, vegaPublicKey: string) {
return this.contract.remove_stake(amount, vegaPublicKey);
return this.contract.remove_stake(amount, prepend0x(vegaPublicKey));
}
stakeBalance(address: string, vegaPublicKey: string) {
return this.contract.stake_balance(address, vegaPublicKey);
return this.contract.stake_balance(address, prepend0x(vegaPublicKey));
}
totalStaked() {
return this.contract.total_staked();

View File

@ -1,6 +0,0 @@
import { hexadecimalify } from './hexadecimalify';
test('Prepends strings with 0x', () => {
expect(hexadecimalify('abc')).toEqual('0xabc');
expect(hexadecimalify('123456789')).toEqual('0x123456789');
});

View File

@ -1,3 +0,0 @@
export function hexadecimalify(str: string) {
return `0x${str}`;
}

View File

@ -1,2 +1,2 @@
export * from './ascii-to-hex';
export * from './hexadecimalify';
export * from './prepend-0x';

View File

@ -0,0 +1,6 @@
import { prepend0x } from './prepend-0x';
test('Prepends strings with 0x', () => {
expect(prepend0x('abc')).toEqual('0xabc');
expect(prepend0x('123456789')).toEqual('0x123456789');
});

View File

@ -0,0 +1,3 @@
export function prepend0x(str: string) {
return `0x${str}`;
}