@kodex-react/ctx-ethers
Version:
Provides a React context provider for the Ethers.js library, which allows you to interact with Ethereum smart contracts.
509 lines (493 loc) • 19 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
require('@kodex-data/prototypes');
require('@kodex-react/window-ethereum');
var React = require('react');
var bytes = require('@ethersproject/bytes');
var strings = require('@ethersproject/strings');
var keccak256 = require('@ethersproject/keccak256');
var providers = require('@ethersproject/providers');
var lodash = require('lodash');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function useEthers() {
return React.useContext(CtxEthers);
}
function useForceUpdate() {
const [, setTick] = React.useState(0);
const update = React.useCallback(() => {
setTick((tick) => tick + 1);
}, []);
return update;
}
// See also: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, intervalDuration, startImmediate = false) {
const [intervalId, setIntervalId] = React.useState(null);
const [isRunning, setIsRunning] = React.useState(startImmediate);
const savedCallback = React.useRef();
const start = () => {
!isRunning && setIsRunning(true);
};
const stop = () => {
isRunning && setIsRunning(false);
};
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
});
// Set up the interval.
React.useEffect(() => {
const tick = () => {
savedCallback.current && savedCallback.current();
};
if (intervalDuration !== null && isRunning) {
let id = setInterval(tick, intervalDuration);
setIntervalId(id);
return () => {
clearInterval(id);
};
}
}, [intervalDuration, isRunning]);
return {
start,
stop,
intervalId
};
}
var initial = {
errors: []
};
const reducer = (previousState, action) => {
let state = Object.assign({}, previousState);
switch (action.type) {
case 'reset':
state = initial;
break;
case 'set-all':
state = action.payload;
break;
case 'set-is-connected':
state.isConnected = action.payload;
break;
case 'set-chain-id':
state.chainId = action.payload;
break;
case 'set-block-number':
state.blockNumber = action.payload;
break;
case 'set-address':
state.address = action.payload;
break;
case 'set-balance':
state.balance = action.payload;
break;
case 'set-nonce':
state.nonce = action.payload;
break;
case 'set-gas-limit':
state.gasLimit = action.payload;
break;
case 'set-block-time':
state.blockTime = action.payload;
break;
case 'set-no-injected-provider':
state.noInjectedProvider = action.payload;
break;
case 'add-error':
state.errors.push(action.payload);
break;
case 'remove-error':
state.errors = state.errors.filter((_, i) => i !== action.index);
break;
case 'set-errors':
state.errors = action.payload;
break;
case 'set-block':
state.block = action.payload;
if (action.payload)
state.gasLimit = action.payload.gasLimit;
break;
}
return state;
};
const REGEX_ERROR_TRANSACTION = /(transaction=)(.*?)(, )/gm;
const REGEX_JSON_KEY_PARAMS = /("params":)(.*?)(,"jsonrpc")/gm;
const REGEX_JSON_KEY_METHOD = /("method":")(.*?)(")/gm;
const REGEX_JSON_KEY_DATA = /("data":")(.*?)(")/gm;
const REGEX_JSON_KEY_FROM = /("from":")(.*?)(")/gm;
const REGEX_JSON_KEY_TO = /("to": ")(.*?)(")/gm;
function _parseParams(message) {
let m;
let result;
while ((m = REGEX_JSON_KEY_PARAMS.exec(message)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === REGEX_JSON_KEY_PARAMS.lastIndex) {
REGEX_JSON_KEY_PARAMS.lastIndex++;
}
m.forEach((match, _) => {
if (!result && match.isValidJson()) {
result = match;
}
});
}
return result;
}
function _parseMethod(message) {
let m;
let result;
while ((m = REGEX_JSON_KEY_METHOD.exec(message)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === REGEX_JSON_KEY_METHOD.lastIndex) {
REGEX_JSON_KEY_METHOD.lastIndex++;
}
m.forEach((match, groupIndex) => {
if (groupIndex === 2 && !result && match) {
result = match;
}
});
}
return result;
}
function _parseData(message) {
let m;
let result;
while ((m = REGEX_JSON_KEY_DATA.exec(message)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === REGEX_JSON_KEY_DATA.lastIndex) {
REGEX_JSON_KEY_DATA.lastIndex++;
}
m.forEach((match, groupIndex) => {
if (groupIndex === 2 && !result && match) {
result = match;
}
});
}
return result;
}
function _parseFrom(message) {
let m;
let result;
while ((m = REGEX_JSON_KEY_FROM.exec(message)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === REGEX_JSON_KEY_FROM.lastIndex) {
REGEX_JSON_KEY_FROM.lastIndex++;
}
m.forEach((match, groupIndex) => {
if (groupIndex === 2 && !result && match.isAddress()) {
result = match;
}
});
}
return result;
}
function _parseTo(message) {
let m;
let result;
while ((m = REGEX_JSON_KEY_TO.exec(message)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === REGEX_JSON_KEY_TO.lastIndex) {
REGEX_JSON_KEY_TO.lastIndex++;
}
m.forEach((match, groupIndex) => {
if (groupIndex === 2 && !result && match.isAddress()) {
result = match;
}
});
}
return result;
}
function _matchTxJSONEthersErr(str) {
// EXAMPLE: `Uncaught (in promise) Error: user rejected transaction (action="sendTransaction", transaction={"data":"0x5c19a95c00000000000000000000000020851e9c716cfb689aff56ac0ce1ac66c874acac","to":"0x09eac2100FF33c3083a822F8DCe9f92415b77B48","from":"0x20851E9C716cFB689AFF56Ac0ce1ac66c874acac","gasLimit":{"type":"BigNumber","hex":"0x017d6b"}}, code=ACTION_REJECTED, version=providers/5.7.2)`
let m;
let result;
while ((m = REGEX_ERROR_TRANSACTION.exec(str)) !== null) {
// This is necessary to avoid infinite loops with zero-width matches
if (m.index === REGEX_ERROR_TRANSACTION.lastIndex) {
REGEX_ERROR_TRANSACTION.lastIndex++;
}
m.forEach((match, _) => {
if (!result && match.isValidJson()) {
result = match;
}
});
}
return result;
}
function parseErrorRegex(message) {
if (message instanceof Error)
message = message.message;
const txJson = _matchTxJSONEthersErr(message);
const method = _parseMethod(message);
const params = _parseParams(message);
const data = _parseData(message);
const from = _parseFrom(message);
const to = _parseTo(message);
return { method, params, data, from, to, txJson };
}
function getRevertReason(tx, provider) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!provider)
return;
try {
if (typeof tx === 'string')
tx = yield provider.getTransaction(tx);
const response = yield provider.call({
to: tx.to,
from: tx.from,
nonce: tx.nonce,
gasLimit: tx.gasLimit,
gasPrice: tx.gasPrice,
data: tx.data,
value: tx.value,
chainId: tx.chainId,
type: (_a = tx.type) !== null && _a !== void 0 ? _a : undefined,
accessList: tx.accessList
}, tx.blockNumber);
return strings.toUtf8String('0x' + response.substring(138));
}
catch (error) {
console.error(`cannot get revert reason`, error);
}
return;
});
}
const CtxEthers = React__namespace.createContext({});
const EthersContext = ({ children, autoEnable, disableChainReload }) => {
const update = useForceUpdate();
const [state, dispatch] = React__namespace.useReducer(reducer, initial);
const [provider, setProvider] = React__namespace.useState();
const [blockTimes, setBlockTimes] = React__namespace.useState([]);
const web3 = React__namespace.useMemo(() => {
if (!state.chainId)
return;
return new providers.Web3Provider(window.ethereum);
}, [state.chainId]);
function watchAsset(address, symbol, decimals, image) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield window.ethereum.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20',
options: {
address,
symbol,
decimals,
image // A string url of the token logo
}
}
});
}
catch (error) {
console.error(error);
}
});
}
function sign(message) {
return __awaiter(this, void 0, void 0, function* () {
if (!provider)
return;
const signer = provider.getSigner();
return {
address: yield signer.getAddress(),
signature: yield signer.signMessage(bytes.arrayify(message))
};
});
}
function sha3(input) {
return keccak256.keccak256(strings.toUtf8Bytes(input));
}
function checkConnected() {
let isConnected = false;
if (window && window.ethereum && window.ethereum.isConnected) {
isConnected = window.ethereum.isConnected();
}
if (state.isConnected !== isConnected) {
dispatch({ type: 'set-is-connected', payload: isConnected });
}
if (!isConnected && provider) {
setProvider(undefined);
// dispatch({ type: 'reset' })
}
return isConnected;
}
function enable() {
return __awaiter(this, void 0, void 0, function* () {
if (typeof window.ethereum.request === 'undefined') {
dispatch({ type: 'set-no-injected-provider', payload: true });
return;
}
dispatch({ type: 'set-no-injected-provider', payload: false });
const { request } = window.ethereum;
const accounts = yield request({ method: 'eth_requestAccounts' });
checkConnected();
if (!accounts[0].isAddress())
return;
dispatch({ type: 'set-address', payload: accounts[0].toChecksumAddress() });
setProvider(new providers.Web3Provider(window.ethereum));
});
}
function handleAccountsChanged(accounts) {
checkConnected();
dispatch({ type: 'set-address', payload: accounts[0].toChecksumAddress() });
}
function handleChainIdChanged(chainId) {
// @ts-ignore
if (disableChainReload !== true)
return window.location.reload(true);
dispatch({ type: 'set-chain-id', payload: parseInt(chainId) });
setProvider(new providers.Web3Provider(window.ethereum));
}
function handleBlocknumber(blocknumber) {
return __awaiter(this, void 0, void 0, function* () {
dispatch({ type: 'set-block-number', payload: blocknumber });
const o = Object.assign({}, state.block);
const n = yield provider.getBlock(blocknumber);
if (o && o.timestamp && o.number) {
const blockTime = (n.timestamp - o.timestamp) / (n.number - o.number);
if (!isNaN(blockTime)) {
const currentBlockTimes = [blockTime, ...blockTimes].slice(0, 20);
const averageBlockTime = lodash.meanBy(currentBlockTimes);
if (!isNaN(averageBlockTime)) {
setBlockTimes(currentBlockTimes);
dispatch({ type: 'set-block-time', payload: averageBlockTime });
}
}
}
dispatch({ type: 'set-block', payload: n });
update();
});
}
function estimateSecondsLeft(toBlock) {
if (!state.blockNumber || !state.blockTime)
return;
const blockRange = toBlock - state.blockNumber;
return blockRange * state.blockTime;
}
function switchChain(chainId) {
const method = 'wallet_switchEthereumChain';
const params = [{ chainId: `0x${chainId.toString(16)}` }];
return window.ethereum.request({ method, params });
}
function addChain(options) {
const method = 'wallet_addEthereumChain';
return window.ethereum.request({ method, params: [options] });
}
function isUnlocked() {
return __awaiter(this, void 0, void 0, function* () {
return yield window.ethereum._metamask.isUnlocked();
});
}
function handleAddressChange(address) {
return __awaiter(this, void 0, void 0, function* () {
if (!provider)
return;
const balance = yield provider.getBalance(address);
const nonce = yield provider.getTransactionCount(address);
dispatch({ type: 'set-balance', payload: balance });
dispatch({ type: 'set-nonce', payload: nonce });
});
}
function getRevertReason$1(tx) {
return __awaiter(this, void 0, void 0, function* () {
if (!provider)
return;
return getRevertReason(tx, provider);
});
}
React__namespace.useEffect(() => {
const __session_chain = sessionStorage.getItem('api_chain');
dispatch({ type: 'set-no-injected-provider', payload: typeof window.ethereum.request === 'undefined' });
if (__session_chain)
enable();
}, []);
React__namespace.useEffect(() => {
if (!state.noInjectedProvider && autoEnable)
enable();
//eslint-disable-next-line
}, [state.noInjectedProvider]);
React__namespace.useEffect(() => {
if (!provider)
return;
checkConnected();
const { chainId } = window.ethereum;
dispatch({ type: 'set-chain-id', payload: parseInt(chainId) });
provider.getBlock('latest').then((latest) => __awaiter(void 0, void 0, void 0, function* () {
const { timestamp } = yield provider.getBlock(latest.number - 30);
const payload = Math.round((latest.timestamp - timestamp) / 30);
dispatch({ type: 'set-block', payload: latest });
dispatch({ type: 'set-block-time', payload });
}));
window.ethereum.on('accountsChanged', handleAccountsChanged);
window.ethereum.on('chainChanged', handleChainIdChanged);
provider.on('block', handleBlocknumber);
return () => {
window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
window.ethereum.removeListener('chainChanged', handleChainIdChanged);
provider.off('block', handleBlocknumber);
};
//eslint-disable-next-line
}, [provider]);
React__namespace.useEffect(() => {
if (!provider)
return;
if (!state.address || !state.address.isAddress())
return;
handleAddressChange(state.address);
}, [state.address, provider]);
useInterval(checkConnected, 500, true);
//prettier-ignore
return (React__namespace.createElement(CtxEthers.Provider, { value: {
state, dispatch, provider, enable, web3,
sha3, estimateSecondsLeft, getRevertReason: getRevertReason$1,
switchChain, isUnlocked, addChain, sign,
watchAsset,
} }, children));
};
const VERSION = '0.0.1-rc.5';
exports.CtxEthers = CtxEthers;
exports.EthersContext = EthersContext;
exports.VERSION = VERSION;
exports.default = EthersContext;
exports.getRevertReason = getRevertReason;
exports.parseErrorRegex = parseErrorRegex;
exports.useEthers = useEthers;
exports.useForceUpdate = useForceUpdate;
exports.useInterval = useInterval;