selectors.ts
1 import erc20 from 'libs/erc20'; 2 import BN from 'bn.js'; 3 import EthTx from 'ethereumjs-tx'; 4 import { bufferToHex } from 'ethereumjs-util'; 5 6 import { Address, Wei, TokenValue, Nonce, getDecimalFromEtherUnit } from 'libs/units'; 7 import { 8 EAC_SCHEDULING_CONFIG, 9 EAC_ADDRESSES, 10 calcEACEndowment, 11 getSchedulerAddress, 12 getScheduleData, 13 getValidateRequestParamsData 14 } from 'libs/scheduling'; 15 import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/transaction'; 16 import { stripHexPrefixAndLower } from 'libs/formatters'; 17 import { SecureWalletName, WalletName, getAddressMessage, AddressMessage } from 'config'; 18 import { Token } from 'types/network'; 19 import { ICurrentTo, ICurrentValue, IGetTransaction } from './types'; 20 import { AppState } from './reducers'; 21 import { addressBookSelectors } from './addressBook'; 22 import { walletTypes, walletSelectors } from './wallet'; 23 import { ratesSelectors } from './rates'; 24 import { customTokensSelectors } from './customTokens'; 25 import { scheduleSelectors, scheduleHelpers } from './schedule'; 26 import { transactionsSelectors } from './transactions'; 27 import { SavedTransaction } from 'types/transactions'; 28 import * as configMetaSelectors from './config/meta/selectors'; 29 import * as configSelectors from './config/selectors'; 30 import * as transactionSelectors from './transaction/selectors'; 31 import * as transactionFieldsSelectors from './transaction/fields/selectors'; 32 import * as transactionMetaSelectors from './transaction/meta/selectors'; 33 import * as transactionSignTypes from './transaction/sign/types'; 34 import * as transactionSignSelectors from './transaction/sign/selectors'; 35 import { reduceToValues, isFullTx } from './helpers'; 36 37 export const isAnyOfflineWithWeb3 = (state: AppState): boolean => { 38 const { isWeb3Wallet } = walletSelectors.getWalletType(state); 39 const offline = configMetaSelectors.getOffline(state); 40 return offline && isWeb3Wallet; 41 }; 42 43 // TODO: Convert to reselect selector (Issue #884) 44 export function getDisabledWallets(state: AppState): any { 45 const network = configSelectors.getNetworkConfig(state); 46 const isOffline = configMetaSelectors.getOffline(state); 47 const disabledWallets: any = { 48 wallets: [], 49 reasons: {} 50 }; 51 52 const addReason = (wallets: WalletName[], reason: string) => { 53 if (!wallets.length) { 54 return; 55 } 56 57 disabledWallets.wallets = disabledWallets.wallets.concat(wallets); 58 wallets.forEach(wallet => { 59 disabledWallets.reasons[wallet] = reason; 60 }); 61 }; 62 63 // Some wallets don't support some networks 64 addReason( 65 configSelectors.unSupportedWalletFormatsOnNetwork(state), 66 `This wallet doesn’t support the ${network.name} network` 67 ); 68 69 // Some wallets are unavailable offline 70 if (isOffline) { 71 addReason( 72 [SecureWalletName.WEB3, SecureWalletName.TREZOR, SecureWalletName.SAFE_T], 73 'This wallet cannot be accessed offline' 74 ); 75 } 76 77 // Some wallets are disabled on certain platforms 78 if (process.env.BUILD_ELECTRON) { 79 addReason([SecureWalletName.WEB3], 'This wallet is not supported in the MyCrypto app'); 80 addReason( 81 [SecureWalletName.SAFE_T], 82 'Coming soon. Please use the MyCrypto.com website in the meantime' 83 ); 84 } 85 86 // Dedupe and sort for consistency 87 disabledWallets.wallets = disabledWallets.wallets 88 .filter((name: string, idx: number) => disabledWallets.wallets.indexOf(name) === idx) 89 .sort(); 90 91 return disabledWallets; 92 } 93 94 export function getTokens(state: AppState): walletTypes.MergedToken[] { 95 const network = configSelectors.getStaticNetworkConfig(state); 96 const tokens: Token[] = network ? network.tokens : []; 97 return tokens.concat( 98 state.customTokens.map((token: Token) => { 99 const mergedToken = { ...token, custom: true }; 100 return mergedToken; 101 }) 102 ) as walletTypes.MergedToken[]; 103 } 104 105 export function getWalletConfigTokens(state: AppState): walletTypes.MergedToken[] { 106 const tokens = getTokens(state); 107 const config = walletSelectors.getWalletConfig(state); 108 if (!config || !config.tokens) { 109 return []; 110 } 111 return config.tokens 112 .map(symbol => tokens.find(t => t.symbol === symbol)) 113 .filter(token => token) as walletTypes.MergedToken[]; 114 } 115 116 export const getToken = (state: AppState, unit: string): walletTypes.MergedToken | undefined => { 117 const tokens = getTokens(state); 118 const token = tokens.find(t => t.symbol === unit); 119 return token; 120 }; 121 122 export function getTokenBalances( 123 state: AppState, 124 nonZeroOnly: boolean = false 125 ): walletTypes.TokenBalance[] { 126 const tokens = getTokens(state); 127 if (!tokens) { 128 return []; 129 } 130 const ret = tokens.map(t => ({ 131 symbol: t.symbol, 132 balance: state.wallet.tokens[t.symbol] 133 ? state.wallet.tokens[t.symbol].balance 134 : TokenValue('0'), 135 error: state.wallet.tokens[t.symbol] ? state.wallet.tokens[t.symbol].error : null, 136 custom: t.custom, 137 decimal: t.decimal 138 })); 139 140 return nonZeroOnly ? ret.filter(t => !t.balance.isZero()) : ret; 141 } 142 143 export const getTokenWithBalance = (state: AppState, unit: string): walletTypes.TokenBalance => { 144 const tokens = getTokenBalances(state, false); 145 const currentToken = tokens.filter(t => t.symbol === unit); 146 //TODO: getting the first index is kinda hacky 147 return currentToken[0]; 148 }; 149 150 export const getTokenBalance = (state: AppState, unit: string): TokenValue | null => { 151 const token = getTokenWithBalance(state, unit); 152 if (!token) { 153 return token; 154 } 155 return token.balance; 156 }; 157 158 export function getShownTokenBalances( 159 state: AppState, 160 nonZeroOnly: boolean = false 161 ): walletTypes.TokenBalance[] { 162 const tokenBalances = getTokenBalances(state, nonZeroOnly); 163 const walletConfig = walletSelectors.getWalletConfig(state); 164 165 let walletTokens: string[] = []; 166 if (walletConfig) { 167 if (walletConfig.tokens) { 168 walletTokens = walletConfig.tokens; 169 } 170 } 171 172 return tokenBalances.filter(t => walletTokens.includes(t.symbol)); 173 } 174 175 const getUSDConversionRate = (state: AppState, unit: string) => { 176 const { isTestnet } = configSelectors.getNetworkConfig(state); 177 const { rates } = ratesSelectors.getRates(state); 178 if (isTestnet) { 179 return null; 180 } 181 182 const conversionRate = rates[unit]; 183 184 if (!conversionRate) { 185 return null; 186 } 187 return conversionRate.USD; 188 }; 189 190 export const getValueInUSD = (state: AppState, value: TokenValue | Wei) => { 191 const unit = getUnit(state); 192 const conversionRate = getUSDConversionRate(state, unit); 193 if (!conversionRate) { 194 return null; 195 } 196 const sendValueUSD = value.muln(conversionRate); 197 return sendValueUSD; 198 }; 199 export const getTransactionFeeInUSD = (state: AppState, fee: Wei) => { 200 const { unit } = configSelectors.getNetworkConfig(state); 201 const conversionRate = getUSDConversionRate(state, unit); 202 203 if (!conversionRate) { 204 return null; 205 } 206 207 const feeValueUSD = fee.muln(conversionRate); 208 return feeValueUSD; 209 }; 210 211 export interface AllUSDValues { 212 valueUSD: BN | null; 213 feeUSD: BN | null; 214 totalUSD: BN | null; 215 } 216 217 export const getAllUSDValuesFromSerializedTx = (state: AppState): AllUSDValues => { 218 const fields = getParamsFromSerializedTx(state); 219 if (!fields) { 220 return { 221 feeUSD: null, 222 valueUSD: null, 223 totalUSD: null 224 }; 225 } 226 const { currentValue, fee } = fields; 227 const valueUSD = getValueInUSD(state, currentValue); 228 const feeUSD = getTransactionFeeInUSD(state, fee); 229 return { 230 feeUSD, 231 valueUSD, 232 totalUSD: feeUSD && valueUSD ? valueUSD.add(feeUSD) : null 233 }; 234 }; 235 236 export function getRecentNetworkTransactions(state: AppState): SavedTransaction[] { 237 const txs = transactionsSelectors.getRecentTransactions(state); 238 const network = configSelectors.getNetworkConfig(state); 239 return txs.filter(tx => tx.chainId === network.chainId); 240 } 241 242 export function getRecentWalletTransactions(state: AppState): SavedTransaction[] { 243 const networkTxs = getRecentNetworkTransactions(state); 244 const wallet = walletSelectors.getWalletInst(state); 245 246 if (wallet) { 247 const addr = wallet.getAddressString().toLowerCase(); 248 return networkTxs.filter(tx => tx.from.toLowerCase() === addr); 249 } else { 250 return []; 251 } 252 } 253 254 export const getSchedulingTransaction = (state: AppState): IGetTransaction => { 255 const { isFullTransaction } = getTransaction(state); 256 257 const currentTo = getCurrentTo(state); 258 const currentValue = getCurrentValue(state); 259 const nonce = transactionFieldsSelectors.getNonce(state); 260 const gasPrice = transactionFieldsSelectors.getGasPrice(state); 261 const timeBounty = scheduleSelectors.getTimeBounty(state); 262 const scheduleGasPrice = scheduleSelectors.getScheduleGasPrice(state); 263 const scheduleGasLimit = scheduleSelectors.getScheduleGasLimit(state); 264 const scheduleType = scheduleSelectors.getScheduleType(state); 265 266 const endowment = calcEACEndowment( 267 scheduleGasLimit.value, 268 currentValue.value, 269 scheduleGasPrice.value, 270 timeBounty.value 271 ); 272 273 let transactionData = null; 274 275 const transactionFullAndValid = isFullTransaction && isSchedulingTransactionValid(state); 276 277 if (transactionFullAndValid) { 278 const deposit = scheduleSelectors.getScheduleDeposit(state); 279 const scheduleTimestamp = scheduleSelectors.getScheduleTimestamp(state); 280 const windowSize = scheduleSelectors.getWindowSize(state); 281 const callData = transactionFieldsSelectors.getData(state); 282 const scheduleTimezone = scheduleSelectors.getScheduleTimezone(state); 283 const windowStart = scheduleSelectors.getWindowStart(state); 284 285 transactionData = getScheduleData( 286 currentTo.raw, 287 callData.raw, 288 scheduleGasLimit.value, 289 currentValue.value, 290 scheduleHelpers.windowSizeBlockToMin(windowSize.value, scheduleType.value), 291 scheduleHelpers.calculateWindowStart( 292 scheduleType.value, 293 scheduleTimestamp, 294 scheduleTimezone.value, 295 windowStart.value 296 ), 297 scheduleGasPrice.value, 298 timeBounty.value, 299 deposit.value 300 ); 301 } 302 303 const transactionOptions = { 304 to: getSchedulerAddress(scheduleType.value), 305 data: transactionData, 306 gasLimit: EAC_SCHEDULING_CONFIG.SCHEDULING_GAS_LIMIT, 307 gasPrice: gasPrice.value, 308 nonce: Nonce('0'), 309 value: endowment 310 }; 311 312 if (nonce) { 313 transactionOptions.nonce = Nonce(nonce.raw); 314 } 315 316 const schedulingTransaction: EthTx = makeTransaction(transactionOptions); 317 318 return { 319 transaction: schedulingTransaction, 320 isFullTransaction: transactionFullAndValid 321 }; 322 }; 323 324 const isSchedulingTransactionValid = (state: AppState): boolean => { 325 const schedulingState = scheduleSelectors.getScheduleState(state); 326 const windowSizeValid = scheduleSelectors.isWindowSizeValid(state); 327 const windowStartValid = scheduleHelpers.isWindowStartValid( 328 schedulingState, 329 configMetaSelectors.getLatestBlock(state) 330 ); 331 const scheduleTimestampValid = scheduleHelpers.isScheduleTimestampValid(schedulingState); 332 const scheduleGasPriceValid = scheduleSelectors.isValidScheduleGasPrice(state); 333 const scheduleGasLimitValid = scheduleSelectors.isValidScheduleGasLimit(state); 334 const depositValid = scheduleSelectors.isValidScheduleDeposit(state); 335 const timeBountyValid = scheduleSelectors.isValidCurrentTimeBounty(state); 336 337 // return true if all fields are valid 338 return ( 339 // either windowStart or scheduleTimestamp is used for scheduling 340 (windowStartValid || scheduleTimestampValid) && 341 windowSizeValid && 342 scheduleGasPriceValid && 343 scheduleGasLimitValid && 344 depositValid && 345 timeBountyValid 346 ); 347 }; 348 349 export interface IGetValidateScheduleParamsCallPayload { 350 to: string; 351 data: string; 352 } 353 354 export const getValidateScheduleParamsCallPayload = ( 355 state: AppState 356 ): IGetValidateScheduleParamsCallPayload | undefined => { 357 const wallet = walletSelectors.getWalletInst(state); 358 const currentTo = getCurrentTo(state); 359 const currentValue = getCurrentValue(state); 360 const timeBounty = scheduleSelectors.getTimeBounty(state); 361 const scheduleGasPrice = scheduleSelectors.getScheduleGasPrice(state); 362 const scheduleGasLimit = scheduleSelectors.getScheduleGasLimit(state); 363 const scheduleType = scheduleSelectors.getScheduleType(state); 364 const deposit = scheduleSelectors.getScheduleDeposit(state); 365 const scheduleTimestamp = scheduleSelectors.getScheduleTimestamp(state); 366 const windowSize = scheduleSelectors.getWindowSize(state); 367 const scheduleTimezone = scheduleSelectors.getScheduleTimezone(state); 368 const windowStart = scheduleSelectors.getWindowStart(state); 369 370 /* 371 * Checks if any of these values are null or invalid 372 * due to an user input. 373 */ 374 if ( 375 !currentValue.value || 376 !currentTo.value || 377 !scheduleGasPrice.value || 378 !wallet || 379 !windowSize.value || 380 // we need either windowStart or scheduleTimestamp for scheduling 381 !(windowStart.value || scheduleTimestamp.value) 382 ) { 383 return; 384 } 385 386 const callGasLimit = scheduleGasLimit.value || EAC_SCHEDULING_CONFIG.SCHEDULE_GAS_LIMIT_FALLBACK; 387 388 const endowment = calcEACEndowment( 389 callGasLimit, 390 currentValue.value, 391 scheduleGasPrice.value, 392 timeBounty.value 393 ); 394 395 const fromAddress = wallet.getAddressString(); 396 397 const data = getValidateRequestParamsData( 398 bufferToHex(currentTo.value), 399 callGasLimit, 400 currentValue.value, 401 scheduleHelpers.windowSizeBlockToMin(windowSize.value, scheduleType.value), 402 scheduleHelpers.calculateWindowStart( 403 scheduleType.value, 404 scheduleTimestamp, 405 scheduleTimezone.value, 406 windowStart.value 407 ), 408 scheduleGasPrice.value, 409 timeBounty.value, 410 deposit.value || Wei('0'), 411 scheduleType.value === 'time', 412 endowment, 413 fromAddress 414 ); 415 416 return { 417 to: EAC_ADDRESSES.KOVAN.requestFactory, 418 data 419 }; 420 }; 421 422 export const isValidCurrentWindowStart = (state: AppState) => { 423 const currentWindowStart = scheduleSelectors.getWindowStart(state); 424 425 if (!currentWindowStart.value) { 426 return false; 427 } 428 429 return currentWindowStart.value > parseInt(configMetaSelectors.getLatestBlock(state), 10); 430 }; 431 432 export const isEtherTransaction = (state: AppState) => { 433 const unit = getUnit(state); 434 const etherUnit = configSelectors.isNetworkUnit(state, unit); 435 return etherUnit; 436 }; 437 438 export const getValidGasCost = (state: AppState) => { 439 const gasCost = transactionSelectors.getGasCost(state); 440 const etherBalance = walletSelectors.getEtherBalance(state); 441 const isOffline = configMetaSelectors.getOffline(state); 442 if (isOffline || !etherBalance) { 443 return true; 444 } 445 return gasCost.lte(etherBalance); 446 }; 447 448 export const getDecimalFromUnit = (state: AppState, unit: string) => { 449 if (configSelectors.isNetworkUnit(state, unit)) { 450 return getDecimalFromEtherUnit('ether'); 451 } else { 452 const token = getToken(state, unit); 453 if (!token) { 454 throw Error(`Token ${unit} not found`); 455 } 456 return token.decimal; 457 } 458 }; 459 460 export const getCurrentTo = (state: AppState): ICurrentTo => 461 isEtherTransaction(state) 462 ? transactionFieldsSelectors.getTo(state) 463 : transactionMetaSelectors.getTokenTo(state); 464 465 export const getCurrentValue = (state: AppState): ICurrentValue => 466 isEtherTransaction(state) 467 ? transactionFieldsSelectors.getValue(state) 468 : transactionMetaSelectors.getTokenValue(state); 469 470 export const isValidCurrentTo = (state: AppState) => { 471 const currentTo = getCurrentTo(state); 472 const dataExists = transactionSelectors.getDataExists(state); 473 if (isEtherTransaction(state)) { 474 // if data exists the address can be 0x 475 return !!currentTo.value || dataExists; 476 } else { 477 return !!currentTo.value; 478 } 479 }; 480 481 export const isCurrentToLabelEntry = (state: AppState): boolean => { 482 const currentTo = getCurrentTo(state); 483 return !currentTo.raw.startsWith('0x'); 484 }; 485 486 export function getCurrentToAddressMessage(state: AppState): AddressMessage | undefined { 487 const to = getCurrentTo(state); 488 return getAddressMessage(to.raw); 489 } 490 491 export const getUnit = (state: AppState) => { 492 const serializedTransaction = getSerializedTransaction(state); 493 const contractInteraction = transactionMetaSelectors.isContractInteraction(state); 494 // attempt to get the to address from the transaction 495 if (serializedTransaction && !contractInteraction) { 496 const transactionInstance = new EthTx(serializedTransaction); 497 const { to } = transactionInstance; 498 if (to) { 499 // see if any tokens match 500 let networkTokens: null | Token[] = null; 501 const customTokens = customTokensSelectors.getCustomTokens(state); 502 const networkConfig = configSelectors.getNetworkConfig(state); 503 if (!networkConfig.isCustom) { 504 networkTokens = networkConfig.tokens; 505 } 506 const mergedTokens = networkTokens ? [...networkTokens, ...customTokens] : customTokens; 507 const toChecksumAddress = configSelectors.getChecksumAddressFn(state); 508 const stringTo = toChecksumAddress(stripHexPrefixAndLower(to.toString('hex'))); 509 const result = mergedTokens.find(t => t.address === stringTo); 510 if (result) { 511 return result.symbol; 512 } 513 } 514 } 515 516 return transactionMetaSelectors.getMetaState(state).unit; 517 }; 518 519 export const signaturePending = (state: AppState) => { 520 const { isHardwareWallet } = walletSelectors.getWalletType(state); 521 const { pending } = state.transaction.sign; 522 return { isHardwareWallet, isSignaturePending: pending }; 523 }; 524 525 export const getSerializedTransaction = (state: AppState) => 526 walletSelectors.getWalletType(state).isWeb3Wallet 527 ? transactionSignSelectors.getWeb3Tx(state) 528 : transactionSignSelectors.getSignedTx(state); 529 530 export const getParamsFromSerializedTx = ( 531 state: AppState 532 ): transactionSignTypes.SerializedTxParams => { 533 const tx = getSerializedTransaction(state); 534 const isEther = isEtherTransaction(state); 535 const decimal = transactionMetaSelectors.getDecimal(state); 536 537 if (!tx) { 538 throw Error('Serialized transaction not found'); 539 } 540 const fields = getTransactionFields(makeTransaction(tx)); 541 const { value, data, gasLimit, gasPrice, to } = fields; 542 const currentValue = isEther ? Wei(value) : TokenValue(erc20.transfer.decodeInput(data)._value); 543 const currentTo = isEther ? Address(to) : Address(erc20.transfer.decodeInput(data)._to); 544 const unit = getUnit(state); 545 const fee = Wei(gasLimit).mul(Wei(gasPrice)); 546 const total = fee.add(Wei(value)); 547 return { ...fields, currentValue, currentTo, fee, total, unit, decimal, isToken: !isEther }; 548 }; 549 550 export const getTransaction = (state: AppState): IGetTransaction => { 551 const currentTo = getCurrentTo(state); 552 const currentValue = getCurrentValue(state); 553 const transactionFields = transactionFieldsSelectors.getFields(state); 554 const unit = getUnit(state); 555 const reducedValues = reduceToValues(transactionFields); 556 const transaction: EthTx = makeTransaction(reducedValues); 557 const dataExists = transactionSelectors.getDataExists(state); 558 const validGasCost = getValidGasCost(state); 559 const isFullTransaction = isFullTx( 560 state, 561 transactionFields, 562 currentTo, 563 currentValue, 564 dataExists, 565 validGasCost, 566 unit 567 ); 568 569 return { transaction, isFullTransaction }; 570 }; 571 572 export const nonStandardTransaction = (state: AppState): boolean => { 573 const etherTransaction = isEtherTransaction(state); 574 const { isFullTransaction } = getTransaction(state); 575 const dataExists = transactionSelectors.getDataExists(state); 576 return isFullTransaction && dataExists && etherTransaction; 577 }; 578 579 export const serializedAndTransactionFieldsMatch = (state: AppState, isLocallySigned: boolean) => { 580 const serialzedTransaction = getSerializedTransaction(state); 581 const { transaction, isFullTransaction } = getTransaction(state); 582 if (!isFullTransaction || !serialzedTransaction) { 583 return false; 584 } 585 const t1 = getTransactionFields(transaction); 586 // inject chainId into t1 as it wont have it from the fields 587 const networkConfig = configSelectors.getNetworkConfig(state); 588 if (!networkConfig) { 589 return false; 590 } 591 const { chainId } = networkConfig; 592 t1.chainId = chainId; 593 594 const t2 = getTransactionFields(makeTransaction(serialzedTransaction)); 595 const checkValidity = (tx: IHexStrTransaction) => 596 Object.keys(tx).reduce( 597 (match, currField: keyof IHexStrTransaction) => match && t1[currField] === t2[currField], 598 true 599 ); 600 //reduce both ways to make sure both are exact same 601 const transactionsMatch = checkValidity(t1) && checkValidity(t2); 602 // if its signed then verify the signature too 603 return transactionsMatch && isLocallySigned 604 ? makeTransaction(serialzedTransaction).verifySignature() 605 : true; 606 }; 607 608 export const getFrom = (state: AppState) => { 609 const serializedTransaction = getSerializedTransaction(state); 610 611 // attempt to get the from address from the transaction 612 if (serializedTransaction) { 613 const transactionInstance = new EthTx(serializedTransaction); 614 615 try { 616 const from = transactionInstance.from; 617 if (from) { 618 const toChecksumAddress = configSelectors.getChecksumAddressFn(state); 619 return toChecksumAddress(from.toString('hex')); 620 } 621 } catch (e) { 622 console.warn(e); 623 } 624 } 625 return transactionMetaSelectors.getMetaState(state).from; 626 }; 627 628 export const getCurrentBalance = (state: AppState): Wei | TokenValue | null => { 629 const etherTransaction = isEtherTransaction(state); 630 if (etherTransaction) { 631 return walletSelectors.getEtherBalance(state); 632 } else { 633 const unit = getUnit(state); 634 return getTokenBalance(state, unit); 635 } 636 }; 637 638 export function getSelectedTokenContractAddress(state: AppState): string { 639 const allTokens = configSelectors.getAllTokens(state); 640 const currentUnit = getUnit(state); 641 642 if (configSelectors.isNetworkUnit(state, currentUnit)) { 643 return ''; 644 } 645 646 return allTokens.reduce((tokenAddr, tokenInfo) => { 647 if (tokenAddr && tokenAddr.length) { 648 return tokenAddr; 649 } 650 651 if (tokenInfo.symbol === currentUnit) { 652 return tokenInfo.address; 653 } 654 655 return tokenAddr; 656 }, ''); 657 } 658 659 export function getCurrentToLabel(state: AppState) { 660 const addresses = addressBookSelectors.getAddressLabels(state); 661 const currentTo = getCurrentTo(state); 662 663 return addresses[currentTo.raw.toLowerCase()] || null; 664 }