edge-core-js
Version:
Edge account & wallet management library
335 lines (289 loc) • 12.2 kB
JavaScript
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import { combinePixies, mapPixie, } from 'redux-pixies'
import { matchJson } from '../../util/match-json'
import {
connectChangeServer
} from './change-server-connection'
import {
walletPixie
} from './wallet/currency-wallet-pixie'
export const currency = combinePixies({
wallets: mapPixie(
walletPixie,
(props) => props.state.currency.currencyWalletIds,
(props, walletId) => ({
...props,
walletId,
walletState: props.state.currency.wallets[walletId],
walletOutput: props.output.currency.wallets[walletId]
})
),
pluginUpdater(input) {
let lastInfo
return async () => {
const { infoCache, plugins } = input.props.state
// Bail out quickly if nothing has changed:
if (lastInfo === infoCache) return
// Update plugins after the first run:
if (lastInfo != null) {
for (const pluginId of Object.keys(plugins.currency)) {
const plugin = plugins.currency[pluginId]
const newPayload = _nullishCoalesce(_optionalChain([infoCache, 'access', _ => _.corePlugins, 'optionalAccess', _2 => _2[pluginId]]), () => ( {}))
const oldPayload = _nullishCoalesce(_optionalChain([lastInfo, 'access', _3 => _3.corePlugins, 'optionalAccess', _4 => _4[pluginId]]), () => ( {}))
if (
plugin.updateInfoPayload != null &&
!matchJson(oldPayload, newPayload)
) {
await plugin.updateInfoPayload(newPayload)
}
}
}
lastInfo = infoCache
}
},
changeServiceManager(input) {
let lastWallets
return async () => {
const { wallets } = input.props.state.currency
// Memoize this function using the wallet state:
if (wallets === lastWallets) {
return
} else {
lastWallets = wallets
}
let { changeService, changeServiceConnected = false } =
_nullishCoalesce(input.props.output.currency.changeServiceManager, () => ( {}))
// The viable wallets that support change server subscriptions
const supportedWallets = Object.entries(wallets).filter(([, wallet]) =>
wallet.changeServiceSubscriptions.some(
subscription => subscription.status !== 'avoiding'
)
)
// Connect the socket if we have 1 or more supported wallets:
if (changeService == null && supportedWallets.length > 0) {
const url = input.props.state.changeServers[0]
changeService = connectChangeServer(url, {
handleChange([pluginId, address, checkpoint]) {
const wallets = Object.entries(input.props.state.currency.wallets)
const filteredWallets = wallets.filter(([, wallet]) => {
return (
wallet.currencyInfo.pluginId === pluginId &&
wallet.changeServiceSubscriptions.some(
subscription => subscription.address === address
)
)
})
for (const [walletId, wallet] of filteredWallets) {
const subscriptions = wallet.changeServiceSubscriptions
.filter(
subscription =>
subscription.address === address &&
subscription.status === 'listening'
)
.map(subscription => ({
...subscription,
status: 'syncing' ,
checkpoint
}))
input.props.dispatch({
type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS',
payload: {
walletId,
subscriptions
}
})
}
},
handleConnect() {
const wallets = Object.entries(input.props.state.currency.wallets)
// Reset to subscribing status for all reconnecting wallets:
for (const [walletId, wallet] of wallets) {
const subscriptions = wallet.changeServiceSubscriptions
.filter(subscription => subscription.status === 'reconnecting')
.map(subscription => ({
...subscription,
status: 'subscribing'
}))
input.props.dispatch({
type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS',
payload: {
walletId,
subscriptions
}
})
}
// Start subscribing for all supported wallets:
input.onOutput({ changeService, changeServiceConnected: true })
},
handleDisconnect() {
const wallets = Object.entries(input.props.state.currency.wallets)
// Reset to reconnecting status for all supported wallets:
for (const [walletId, wallet] of wallets) {
const subscriptions = wallet.changeServiceSubscriptions
.filter(subscription => subscription.status !== 'avoiding')
.map(subscription => ({
...subscription,
status: 'reconnecting'
}))
input.props.dispatch({
type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS',
payload: {
walletId,
subscriptions
}
})
}
input.onOutput({ changeService, changeServiceConnected: false })
},
handleSubLost([pluginId, address]) {
const wallets = Object.entries(input.props.state.currency.wallets)
const filteredWallets = wallets.filter(
([, wallet]) =>
// Wallet must be for the same plugin, because those wallets,
wallet.currencyInfo.pluginId === pluginId &&
// Wallet must have a subscription for the address:
wallet.changeServiceSubscriptions.some(
subscription => subscription.address === address
) &&
// Wallet subscription not be avoiding the change service
// because those wallets shouldn't be affected by this event:
wallet.changeServiceSubscriptions.some(
subscription => subscription.status !== 'avoiding'
)
)
// Set status back to subscribing for all filtered wallets:
for (const [walletId, wallet] of filteredWallets) {
const subscriptions =
wallet.changeServiceSubscriptions
.filter(subscription => subscription.status !== 'avoiding')
.map(subscription => ({
...subscription,
status: 'subscribing'
}))
input.props.dispatch({
type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS',
payload: {
walletId,
subscriptions
}
})
}
}
})
input.onOutput({ changeService, changeServiceConnected: false })
}
// Disconnect the socket if we have 0 supported wallets:
if (changeService != null && supportedWallets.length === 0) {
changeService.close()
changeService = undefined
input.onOutput({ changeService, changeServiceConnected: false })
}
// Subscribe wallets to the change service:
if (_optionalChain([changeService, 'optionalAccess', _5 => _5.connected]) === true && changeServiceConnected) {
const filteredWallets = supportedWallets.filter(([, wallet]) =>
wallet.changeServiceSubscriptions.some(
subscription =>
subscription.status === 'subscribing' ||
subscription.status === 'resubscribing'
)
)
const indexToWalletId
= []
const batches = []
for (const [walletId, wallet] of filteredWallets) {
if (wallet.paused) continue
// Build the subscribe parameters:
const params = wallet.changeServiceSubscriptions.map(
(subscription) => [
wallet.currencyInfo.pluginId,
subscription.address,
subscription.checkpoint
]
)
if (params.length === 0) continue
let subscribeParams = batches[batches.length - 1]
if (
subscribeParams == null ||
subscribeParams.length + params.length > 100
) {
batches.push([])
subscribeParams = batches[batches.length - 1]
}
subscribeParams.push(...params)
for (let i = 0; i < params.length; i++) {
indexToWalletId[indexToWalletId.length] = { walletId, wallet }
}
}
// Subscribe to the change service:
const results = []
for (const subscribeParams of batches) {
const r = await changeService
.subscribe(subscribeParams)
.catch(err => {
input.props.log(`Failed to subscribe: ${String(err)}`)
return [0]
})
results.push(...r)
}
const subscriptionUpdates =
new Map()
for (let i = 0; i < results.length; i++) {
const result = results[i]
const { walletId, wallet } = indexToWalletId[i]
// Determine the new status of the subscription to all addresses
// for the wallet:
let status
switch (result) {
// Change server does not support this wallet plugin:
case -1:
// Avoid the change service:
status = 'avoiding'
break
// Change server does support this wallet plugin, but failed to
// subscribe to the address:
case 0:
// Try subscribing again later:
status = 'resubscribing'
break
// Change server does support this wallet plugin, and there are no
// changes for the address:
case 1:
// Start syncing the wallet once for initial syncNetwork call:
status = 'syncing'
break
// Change server does support this wallet plugin, and there are
// changes for the address:
case 2:
// Start syncing the wallet:
status = 'syncing'
break
}
// Update the status for the subscription:
const subscriptions = wallet.changeServiceSubscriptions
.filter(
subscription =>
subscription.status === 'subscribing' ||
subscription.status === 'resubscribing'
)
.map(subscription => ({
...subscription,
status
}))
subscriptionUpdates.set(walletId, subscriptions)
}
// TODO: Remove the Array.from() once we drop ES5 targeting. This is
// need to avoid iterables because looping over iterables are broken
// for ES5 targets.
const entries = Array.from(subscriptionUpdates.entries())
for (const [walletId, subscriptions] of entries) {
input.props.dispatch({
type: 'CURRENCY_ENGINE_UPDATE_CHANGE_SERVICE_SUBSCRIPTIONS',
payload: {
walletId,
subscriptions
}
})
}
}
}
}
})