edge-core-js
Version:
Edge account & wallet management library
284 lines (246 loc) • 7.66 kB
JavaScript
function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } }import { justFiles } from 'disklet'
import { base64 } from 'rfc4648'
import { makeJsonFile } from '../../util/file-helpers'
import { makeKeyInfo } from '../login/keys'
import { wasEdgeStorageKeys } from '../login/storage-keys'
import {
getStorageWalletDisklet,
hashStorageWalletFilename
} from '../storage/storage-selectors'
import {
asLegacyWalletFile,
asPluginSettingsFile,
asWalletStateFile
} from './account-cleaners'
const legacyWalletFile = makeJsonFile(asLegacyWalletFile)
const walletStateFile = makeJsonFile(asWalletStateFile)
const pluginSettingsFile = makeJsonFile(asPluginSettingsFile)
const emptySettings = asPluginSettingsFile({})
const PLUGIN_SETTINGS_FILE = 'PluginSettings.json'
/**
* Returns true if `Object.assign(a, b)` would alter `a`.
*/
function different(a, b) {
for (const key of Object.keys(b)) {
if (a[key] !== b[key]) {
return true
}
}
return false
}
/**
* Loads the legacy wallet list from the account folder.
*/
async function loadWalletList(disklet) {
const walletInfos = []
const walletStates = {}
const paths = justFiles(await disklet.list('Wallets'))
await Promise.all(
paths.map(async path => {
const clean = await legacyWalletFile.load(disklet, path)
if (clean == null) return
const keys = {
...wasEdgeStorageKeys({
dataKey: clean.MK,
syncKey: clean.SyncKey
}),
bitcoinKey: base64.stringify(clean.BitcoinSeed),
format: 'bip32'
}
const keyInfo = makeKeyInfo('wallet:bitcoin', keys, clean.MK)
walletInfos.push(keyInfo)
walletStates[keyInfo.id] = {
archived: clean.Archived,
deleted: false,
hidden: false,
migratedFromWalletId: undefined,
sortIndex: clean.SortIndex
}
})
)
return { walletInfos, walletStates }
}
/**
* Loads the modern key state list from the account folder.
*/
async function loadWalletStates(disklet) {
const out = {}
const paths = justFiles(await disklet.list('Keys'))
await Promise.all(
paths.map(async path => {
const clean = await walletStateFile.load(disklet, path)
if (clean == null) return
const { id, archived, deleted, hidden, migratedFromWalletId, sortIndex } =
clean
out[id] = { archived, deleted, hidden, sortIndex, migratedFromWalletId }
})
)
return out
}
/**
* Loads the keyStates and legacy wallet list,
* diffs them with the current keyStates and legacy wallet list,
* and returns true if there are any changes.
*/
export async function loadAllWalletStates(
ai,
accountId
) {
const accountState = ai.props.state.accounts[accountId]
const { accountWalletInfo, accountWalletInfos } = accountState
// Read legacy files from all Airbitz repos:
const legacyLists = await Promise.all(
accountWalletInfos.map(info =>
loadWalletList(getStorageWalletDisklet(ai.props.state, info.id))
)
)
// Read states from the primary Edge repo:
const newStates = await loadWalletStates(
getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)
)
// Merge all that information together:
const legacyWalletInfos = []
for (const files of legacyLists) {
legacyWalletInfos.push(...files.walletInfos)
}
const legacyWalletStates = legacyLists.map(
files => files.walletStates
)
const walletStates = Object.assign({}, ...legacyWalletStates, newStates)
ai.props.dispatch({
type: 'ACCOUNT_KEYS_LOADED',
payload: {
accountId,
legacyWalletInfos,
walletStates
}
})
}
/**
* Changes the wallet states within an account.
*/
export async function changeWalletStates(
ai,
accountId,
newStates
) {
const { accountWalletInfo, walletStates } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)
// Find the changes between the new states and the old states:
const toWrite = {}
for (const id of Object.keys(newStates)) {
if (walletStates[id] == null) {
// We don't have this id, so everything is new:
toWrite[id] = newStates[id]
} else if (different(walletStates[id], newStates[id])) {
// We already have this id, so only update if it has changed:
toWrite[id] = { ...walletStates[id], ...newStates[id] }
}
}
// If there are no changes, do nothing:
const walletIds = Object.keys(toWrite)
if (walletIds.length < 1) return
await Promise.all(
walletIds.map(async walletId => {
const { archived, deleted, hidden, migratedFromWalletId, sortIndex } =
toWrite[walletId]
const walletIdHash = hashStorageWalletFilename(
ai.props.state,
accountWalletInfo.id,
walletId
)
await walletStateFile.save(disklet, `Keys/${walletIdHash}.json`, {
archived,
deleted,
hidden,
id: walletId,
migratedFromWalletId,
sortIndex
})
})
)
ai.props.dispatch({
type: 'ACCOUNT_CHANGED_WALLET_STATES',
payload: { accountId, walletStates: { ...walletStates, ...toWrite } }
})
}
/**
* Changes a currency plugin's settings within an account.
*/
export async function changePluginUserSettings(
ai,
accountId,
pluginId,
userSettings
) {
const { accountWalletInfo } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)
// Write the new state to disk:
const clean =
await _asyncNullishCoalesce((await pluginSettingsFile.load(disklet, PLUGIN_SETTINGS_FILE)), async () => (
emptySettings))
await pluginSettingsFile.save(disklet, PLUGIN_SETTINGS_FILE, {
...clean,
userSettings: {
...ai.props.state.accounts[accountId].userSettings,
[pluginId]: userSettings
}
})
// Update Redux:
ai.props.dispatch({
type: 'ACCOUNT_PLUGIN_SETTINGS_CHANGED',
payload: {
accountId,
pluginId,
userSettings: { ...userSettings }
}
})
}
/**
* Enables or disables swap plugins.
*/
export async function changeSwapSettings(
ai,
accountId,
pluginId,
swapSettings
) {
const { accountWalletInfo } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)
// Write the new state to disk:
const clean =
await _asyncNullishCoalesce((await pluginSettingsFile.load(disklet, PLUGIN_SETTINGS_FILE)), async () => (
emptySettings))
await pluginSettingsFile.save(disklet, PLUGIN_SETTINGS_FILE, {
...clean,
swapSettings: {
...ai.props.state.accounts[accountId].swapSettings,
[pluginId]: swapSettings
}
})
// Update Redux:
ai.props.dispatch({
type: 'ACCOUNT_SWAP_SETTINGS_CHANGED',
payload: { accountId, pluginId, swapSettings }
})
}
/**
* Loads the settings for all the currency plugins within an account.
*/
export async function reloadPluginSettings(
ai,
accountId
) {
const { accountWalletInfo } = ai.props.state.accounts[accountId]
const disklet = getStorageWalletDisklet(ai.props.state, accountWalletInfo.id)
const clean = await pluginSettingsFile.load(disklet, PLUGIN_SETTINGS_FILE)
const { userSettings, swapSettings } = _nullishCoalesce(clean, () => ( {
userSettings: {},
swapSettings: {}
}))
// Add the final list to Redux:
ai.props.dispatch({
type: 'ACCOUNT_PLUGIN_SETTINGS_LOADED',
payload: { accountId, userSettings, swapSettings }
})
}