Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/hooks/useWallet.js
import { useState, useEffect } from 'react'; | import { useState, useEffect } from 'react'; | ||||
import useAsyncTimeout from 'hooks/useAsyncTimeout'; | import useAsyncTimeout from 'hooks/useAsyncTimeout'; | ||||
import usePrevious from 'hooks/usePrevious'; | import usePrevious from 'hooks/usePrevious'; | ||||
import useBCH from 'hooks/useBCH'; | import useBCH from 'hooks/useBCH'; | ||||
import BigNumber from 'bignumber.js'; | import BigNumber from 'bignumber.js'; | ||||
import Long from 'long'; | |||||
import { | import { | ||||
fromSmallestDenomination, | fromSmallestDenomination, | ||||
loadStoredWallet, | loadStoredWallet, | ||||
isValidStoredWallet, | isValidStoredWallet, | ||||
isLegacyMigrationRequired, | isLegacyMigrationRequired, | ||||
whichUtxosWereAdded, | whichUtxosWereAdded, | ||||
whichUtxosWereConsumed, | whichUtxosWereConsumed, | ||||
addNewHydratedUtxos, | addNewHydratedUtxos, | ||||
Show All 20 Lines | const useWallet = () => { | ||||
const { | const { | ||||
getBCH, | getBCH, | ||||
getUtxos, | getUtxos, | ||||
getHydratedUtxoDetails, | getHydratedUtxoDetails, | ||||
getSlpBalancesAndUtxos, | getSlpBalancesAndUtxos, | ||||
getTxHistory, | getTxHistory, | ||||
getTxData, | getTxData, | ||||
addTokenTxData, | addTokenTxData, | ||||
getUtxosChronik, | |||||
getSlpBalancesAndUtxosFromChronik, | |||||
addTokenInfo, | |||||
} = useBCH(); | } = useBCH(); | ||||
const [loading, setLoading] = useState(true); | const [loading, setLoading] = useState(true); | ||||
const [apiIndex, setApiIndex] = useState(0); | const [apiIndex, setApiIndex] = useState(0); | ||||
const [BCH, setBCH] = useState(getBCH(apiIndex)); | const [BCH, setBCH] = useState(getBCH(apiIndex)); | ||||
const { balances, tokens, utxos } = isValidStoredWallet(wallet) | const { balances, tokens, utxos } = isValidStoredWallet(wallet) | ||||
? wallet.state | ? wallet.state | ||||
: { | : { | ||||
balances: {}, | balances: {}, | ||||
▲ Show 20 Lines • Show All 56 Lines • ▼ Show 20 Lines | const normalizeBalance = slpBalancesAndUtxos => { | ||||
0, | 0, | ||||
); | ); | ||||
return { | return { | ||||
totalBalanceInSatoshis, | totalBalanceInSatoshis, | ||||
totalBalance: fromSmallestDenomination(totalBalanceInSatoshis), | totalBalance: fromSmallestDenomination(totalBalanceInSatoshis), | ||||
}; | }; | ||||
}; | }; | ||||
const normalizeBalanceChronik = chronikSlpBalancesAndUtxos => { | |||||
const totalBalanceInSatoshis = | |||||
chronikSlpBalancesAndUtxos.nonSlpUtxos.reduce( | |||||
(previousBalance, utxo) => | |||||
parseInt( | |||||
new BigNumber(previousBalance) | |||||
.plus( | |||||
new BigNumber( | |||||
new Long(utxo.value).toString(10), | |||||
), | |||||
) | |||||
.toString(), | |||||
), | |||||
0, | |||||
); | |||||
console.log(`totalBalanceInSatoshisChronik`, totalBalanceInSatoshis); | |||||
return { | |||||
totalBalanceInSatoshis, | |||||
totalBalance: fromSmallestDenomination(totalBalanceInSatoshis), | |||||
}; | |||||
}; | |||||
const deriveAccount = async (BCH, { masterHDNode, path }) => { | const deriveAccount = async (BCH, { masterHDNode, path }) => { | ||||
const node = BCH.HDNode.derivePath(masterHDNode, path); | const node = BCH.HDNode.derivePath(masterHDNode, path); | ||||
const publicKey = BCH.HDNode.toPublicKey(node).toString('hex'); | const publicKey = BCH.HDNode.toPublicKey(node).toString('hex'); | ||||
const cashAddress = BCH.HDNode.toCashAddress(node); | const cashAddress = BCH.HDNode.toCashAddress(node); | ||||
const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress); | const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress); | ||||
return { | return { | ||||
publicKey, | publicKey, | ||||
cashAddress, | cashAddress, | ||||
slpAddress, | slpAddress, | ||||
fundingWif: BCH.HDNode.toWIF(node), | fundingWif: BCH.HDNode.toWIF(node), | ||||
fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), | fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress), | ||||
legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), | legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress), | ||||
}; | }; | ||||
}; | }; | ||||
const loadWalletFromStorageOnStartup = async setWallet => { | const loadWalletFromStorageOnStartup = async setWallet => { | ||||
console.log(`loadWalletFromStorageOnStartup`); | |||||
// get wallet object from localforage | // get wallet object from localforage | ||||
const wallet = await getWallet(); | const wallet = await getWallet(); | ||||
console.log(`loadWalletFromStorageOnStartup wallet`, wallet); | |||||
console.log( | |||||
`loadWalletFromStorageOnStartup isValidStoredWallet(wallet)`, | |||||
isValidStoredWallet(wallet), | |||||
); | |||||
// If wallet object in storage is valid, use it to set state on startup | // If wallet object in storage is valid, use it to set state on startup | ||||
if (isValidStoredWallet(wallet)) { | if (isValidStoredWallet(wallet)) { | ||||
// Convert all the token balance figures to big numbers | // Convert all the token balance figures to big numbers | ||||
const liveWalletState = loadStoredWallet(wallet.state); | const liveWalletState = loadStoredWallet(wallet.state); | ||||
console.log( | |||||
`loadWalletFromStorageOnStartup liveWalletState`, | |||||
liveWalletState, | |||||
); | |||||
wallet.state = liveWalletState; | wallet.state = liveWalletState; | ||||
setWallet(wallet); | setWallet(wallet); | ||||
return setLoading(false); | return setLoading(false); | ||||
} | } | ||||
// Loading will remain true until API calls populate this legacy wallet | // Loading will remain true until API calls populate this legacy wallet | ||||
setWallet(wallet); | setWallet(wallet); | ||||
}; | }; | ||||
const haveUtxosChangedChronik = (utxos, previousUtxos) => { | |||||
bytesofman: NB: this approach is not robust. It just happens to work for a wallet only receiving… | |||||
console.log(`New utxos`, utxos); | |||||
console.log(`Previous utxos`, previousUtxos); | |||||
delete previousUtxos[0].wif; | |||||
delete previousUtxos[previousUtxos.length - 1].wif; | |||||
// TODO this function should have unit tests and be in cashMethods | |||||
// TODO work out the logic of when you want to force the calc anyway, i.e. !wallet you might want to be true | |||||
// Utxo set has changed if first or last utxo is not the same | |||||
const firstUtxo = JSON.stringify(utxos[0]); | |||||
const lastUtxo = JSON.stringify(utxos[utxos.length - 1]); | |||||
const previousFirstUtxo = JSON.stringify(previousUtxos[0]); | |||||
const previousLastUtxo = JSON.stringify( | |||||
previousUtxos[previousUtxos.length - 1], | |||||
); | |||||
console.log(`firstUtxo`, firstUtxo); | |||||
console.log(`lastUtxo`, lastUtxo); | |||||
console.log(`previousFirstUtxo`, previousFirstUtxo); | |||||
console.log(`previousLastUtxo`, previousLastUtxo); | |||||
return ( | |||||
(firstUtxo !== previousFirstUtxo) | (lastUtxo !== previousLastUtxo) | |||||
); | |||||
}; | |||||
const haveUtxosChanged = (wallet, utxos, previousUtxos) => { | const haveUtxosChanged = (wallet, utxos, previousUtxos) => { | ||||
// Relevant points for this array comparing exercise | // Relevant points for this array comparing exercise | ||||
// https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why | // https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why | ||||
// https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript | // https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript | ||||
// If this is initial state | // If this is initial state | ||||
if (utxos === null) { | if (utxos === null) { | ||||
// Then make sure to get slpBalancesAndUtxos | // Then make sure to get slpBalancesAndUtxos | ||||
Show All 36 Lines | const haveUtxosChanged = (wallet, utxos, previousUtxos) => { | ||||
return true; | return true; | ||||
} | } | ||||
} | } | ||||
// Compare utxo sets | // Compare utxo sets | ||||
return !isEqual(utxos, utxosToCompare); | return !isEqual(utxos, utxosToCompare); | ||||
}; | }; | ||||
const chronikUpdate = async ({ wallet }) => { | |||||
console.log(`chronikUpdate`); | |||||
//console.log(`tick()`); | |||||
console.time('chronikUpdate'); | |||||
console.log(`wallet`, wallet); | |||||
console.log(`isValidStoredWallet(wallet)`, isValidStoredWallet(wallet)); | |||||
try { | |||||
if (!wallet) { | |||||
return; | |||||
} | |||||
const cashAddresses = [ | |||||
wallet.Path245.cashAddress, | |||||
wallet.Path145.cashAddress, | |||||
wallet.Path1899.cashAddress, | |||||
]; | |||||
const publicKeys = [ | |||||
wallet.Path145.publicKey, | |||||
wallet.Path245.publicKey, | |||||
wallet.Path1899.publicKey, | |||||
]; | |||||
const utxosChronik = await getUtxosChronik(BCH, cashAddresses); | |||||
console.log(`utxosChronik`, utxosChronik); | |||||
const chronikUtxosHaveChanged = haveUtxosChangedChronik( | |||||
utxosChronik, | |||||
wallet.state.utxos, | |||||
); | |||||
console.log(`chronikUtxosHaveChanged`, chronikUtxosHaveChanged); | |||||
// If utxo set is unchanged, no need to update wallet state | |||||
if (!chronikUtxosHaveChanged) { | |||||
console.log(`utxo set has not changed, wallet state unchanged`); | |||||
// remove api error here; otherwise it will remain if recovering from a rate | |||||
// limit error with an unchanged utxo set | |||||
setApiError(false); | |||||
console.timeEnd('chronikUpdate'); | |||||
return; | |||||
} | |||||
let chronikSlpBalancesAndUtxos = | |||||
getSlpBalancesAndUtxosFromChronik(utxosChronik); | |||||
const chronikBalances = normalizeBalanceChronik( | |||||
chronikSlpBalancesAndUtxos, | |||||
); | |||||
console.log(`chronikBalances`, chronikBalances); | |||||
const tokensWithGenesisInfo = await addTokenInfo( | |||||
chronikSlpBalancesAndUtxos.tokens, | |||||
); | |||||
chronikSlpBalancesAndUtxos.tokens = tokensWithGenesisInfo; | |||||
chronikSlpBalancesAndUtxos = normalizeSlpBalancesAndUtxos( | |||||
chronikSlpBalancesAndUtxos, | |||||
wallet, | |||||
); | |||||
console.log( | |||||
`chronikSlpBalancesAndUtxos`, | |||||
chronikSlpBalancesAndUtxos, | |||||
); | |||||
// tx history is still legacy here | |||||
const txHistory = await getTxHistory(BCH, cashAddresses); | |||||
// public keys are used to determined if a tx is incoming outgoing | |||||
const parsedTxHistory = await getTxData( | |||||
BCH, | |||||
txHistory, | |||||
publicKeys, | |||||
wallet, | |||||
); | |||||
const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory); | |||||
if (typeof chronikSlpBalancesAndUtxos === 'undefined') { | |||||
console.log(`slpBalancesAndUtxos is undefined`); | |||||
throw new Error('slpBalancesAndUtxos is undefined'); | |||||
} | |||||
const newState = { | |||||
balances: {}, | |||||
tokens: [], | |||||
slpBalancesAndUtxos: [], | |||||
}; | |||||
newState.slpBalancesAndUtxos = chronikSlpBalancesAndUtxos; | |||||
newState.balances = chronikBalances; | |||||
newState.tokens = tokensWithGenesisInfo; | |||||
newState.parsedTxHistory = parsedWithTokens; | |||||
newState.utxos = utxosChronik; | |||||
newState.hydratedUtxoDetails = utxosChronik; | |||||
console.log(`new state`, newState); | |||||
// Set wallet with new state field | |||||
wallet.state = newState; | |||||
setWallet(wallet); | |||||
// Write this state to indexedDb using localForage | |||||
writeWalletState(wallet, newState); | |||||
// If everything executed correctly, remove apiError | |||||
setApiError(false); | |||||
} catch (error) { | |||||
console.log(`Error in update({wallet})`); | |||||
console.log(error); | |||||
// Set this in state so that transactions are disabled until the issue is resolved | |||||
setApiError(true); | |||||
console.timeEnd('chronikUpdate'); | |||||
// Try another endpoint | |||||
console.log(`Trying next API...`); | |||||
tryNextAPI(); | |||||
} | |||||
console.timeEnd('chronikUpdate'); | |||||
}; | |||||
const update = async ({ wallet }) => { | const update = async ({ wallet }) => { | ||||
//console.log(`tick()`); | //console.log(`tick()`); | ||||
//console.time("update"); | //console.time("update"); | ||||
console.log(`update`); | |||||
try { | try { | ||||
if (!wallet) { | if (!wallet) { | ||||
return; | return; | ||||
} | } | ||||
const cashAddresses = [ | const cashAddresses = [ | ||||
wallet.Path245.cashAddress, | wallet.Path245.cashAddress, | ||||
wallet.Path145.cashAddress, | wallet.Path145.cashAddress, | ||||
wallet.Path1899.cashAddress, | wallet.Path1899.cashAddress, | ||||
]; | ]; | ||||
const publicKeys = [ | const publicKeys = [ | ||||
wallet.Path145.publicKey, | wallet.Path145.publicKey, | ||||
wallet.Path245.publicKey, | wallet.Path245.publicKey, | ||||
wallet.Path1899.publicKey, | wallet.Path1899.publicKey, | ||||
]; | ]; | ||||
const utxos = await getUtxos(BCH, cashAddresses); | const utxos = await getUtxos(BCH, cashAddresses); | ||||
//Chronik testing | |||||
const utxosChronik = await getUtxosChronik(BCH, cashAddresses); | |||||
console.log(`utxosChronik`, utxosChronik); | |||||
// TODO | |||||
// write a function to turn this into slpBalancesAndUtxos | |||||
let chronikSlpBalancesAndUtxos = | |||||
getSlpBalancesAndUtxosFromChronik(utxosChronik); | |||||
console.log( | |||||
`chronikSlpBalancesAndUtxos`, | |||||
chronikSlpBalancesAndUtxos, | |||||
); | |||||
const chronikBalances = normalizeBalanceChronik( | |||||
chronikSlpBalancesAndUtxos, | |||||
); | |||||
console.log(`chronikBalances`, chronikBalances); | |||||
const tokensWithGenesisInfo = await addTokenInfo( | |||||
chronikSlpBalancesAndUtxos.tokens, | |||||
); | |||||
chronikSlpBalancesAndUtxos.tokens = tokensWithGenesisInfo; | |||||
chronikSlpBalancesAndUtxos = normalizeSlpBalancesAndUtxos( | |||||
chronikSlpBalancesAndUtxos, | |||||
wallet, | |||||
); | |||||
console.log( | |||||
`chronikSlpBalancesAndUtxos with info + normalized`, | |||||
chronikSlpBalancesAndUtxos, | |||||
); | |||||
// next steps | |||||
// You still need to get tokenQty right from tokenDecimals | |||||
// make sure you can get xec balance from your utxos | |||||
// make sure utxos have all the info you need to send txs | |||||
// If an error is returned or utxos from only 1 address are returned | // If an error is returned or utxos from only 1 address are returned | ||||
if (!utxos || isEmpty(utxos) || utxos.error || utxos.length < 2) { | if (!utxos || isEmpty(utxos) || utxos.error || utxos.length < 2) { | ||||
// Throw error here to prevent more attempted api calls | // Throw error here to prevent more attempted api calls | ||||
// as you are likely already at rate limits | // as you are likely already at rate limits | ||||
throw new Error('Error fetching utxos'); | throw new Error('Error fetching utxos'); | ||||
} | } | ||||
// Need to call with wallet as a parameter rather than trusting it is in state, otherwise can sometimes get wallet=false from haveUtxosChanged | // Need to call with wallet as a parameter rather than trusting it is in state, otherwise can sometimes get wallet=false from haveUtxosChanged | ||||
const utxosHaveChanged = haveUtxosChanged( | const utxosHaveChanged = haveUtxosChanged( | ||||
wallet, | wallet, | ||||
utxos, | utxos, | ||||
previousUtxos, | previousUtxos, | ||||
); | ); | ||||
// If the utxo set has not changed, | // If the utxo set has not changed, | ||||
if (!utxosHaveChanged) { | if (!utxosHaveChanged) { | ||||
console.log( | |||||
`slpBalancesAndUtxos`, | |||||
wallet.state.slpBalancesAndUtxos, | |||||
); | |||||
// remove api error here; otherwise it will remain if recovering from a rate | // remove api error here; otherwise it will remain if recovering from a rate | ||||
// limit error with an unchanged utxo set | // limit error with an unchanged utxo set | ||||
setApiError(false); | setApiError(false); | ||||
// then wallet.state has not changed and does not need to be updated | // then wallet.state has not changed and does not need to be updated | ||||
//console.timeEnd("update"); | //console.timeEnd("update"); | ||||
return; | return; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 129 Lines • ▼ Show 20 Lines | const update = async ({ wallet }) => { | ||||
console.log(`Trying next API...`); | console.log(`Trying next API...`); | ||||
tryNextAPI(); | tryNextAPI(); | ||||
} | } | ||||
//console.timeEnd("update"); | //console.timeEnd("update"); | ||||
}; | }; | ||||
const getActiveWalletFromLocalForage = async () => { | const getActiveWalletFromLocalForage = async () => { | ||||
let wallet; | let wallet; | ||||
console.log(`getActiveWalletFromLocalForage`); | |||||
try { | try { | ||||
wallet = await localforage.getItem('wallet'); | wallet = await localforage.getItem('wallet'); | ||||
console.log( | |||||
`wallet pulled in getActiveWalletFromLocalForage`, | |||||
wallet, | |||||
); | |||||
// Confirm that balance is a number | |||||
const { balances } = wallet.state; | |||||
const { totalBalance, totalBalanceInSatoshis } = balances; | |||||
console.log(`totalBalance`, totalBalance); | |||||
console.log(`totalBalanceInSatoshis`, totalBalanceInSatoshis); | |||||
console.log(`isNaN(totalBalance)`, isNaN(totalBalance)); | |||||
console.log( | |||||
`isNaN(totalBalanceInSatoshis)`, | |||||
isNaN(totalBalanceInSatoshis), | |||||
); | |||||
if (isNaN(totalBalance) || isNaN(totalBalanceInSatoshis)) { | |||||
console.log(`setting wallet to null`); | |||||
wallet = null; | |||||
} | |||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error in getActiveWalletFromLocalForage`, err); | console.log(`Error in getActiveWalletFromLocalForage`, err); | ||||
wallet = null; | wallet = null; | ||||
} | } | ||||
return wallet; | return wallet; | ||||
}; | }; | ||||
const getContactListFromLocalForage = async () => { | const getContactListFromLocalForage = async () => { | ||||
Show All 14 Lines | const updateContactListInLocalForage = async contactListArray => { | ||||
} catch (err) { | } catch (err) { | ||||
console.log('Error in updateContactListInLocalForage', err); | console.log('Error in updateContactListInLocalForage', err); | ||||
updateSuccess = false; | updateSuccess = false; | ||||
} | } | ||||
return updateSuccess; | return updateSuccess; | ||||
}; | }; | ||||
const getWallet = async () => { | const getWallet = async () => { | ||||
console.log(`getWallet`); | |||||
let wallet; | let wallet; | ||||
let existingWallet; | let existingWallet; | ||||
try { | try { | ||||
existingWallet = await getActiveWalletFromLocalForage(); | existingWallet = await getActiveWalletFromLocalForage(); | ||||
console.log( | |||||
`existingWallet at beginning of getWallet`, | |||||
existingWallet, | |||||
); | |||||
// existing wallet will be | // existing wallet will be | ||||
// 1 - the 'wallet' value from localForage, if it exists | // 1 - the 'wallet' value from localForage, if it exists | ||||
// 2 - false if it does not exist in localForage | // 2 - false if it does not exist in localForage | ||||
// 3 - null if error | // 3 - null if error | ||||
// If the wallet does not have Path1899, add it | // If the wallet does not have Path1899, add it | ||||
// or each Path1899, Path145, Path245 does not have a public key, add them | // or each Path1899, Path145, Path245 does not have a public key, add them | ||||
if (existingWallet) { | if (existingWallet) { | ||||
Show All 35 Lines | const getWallet = async () => { | ||||
} | } | ||||
if (existingWallet === null || !existingWallet) { | if (existingWallet === null || !existingWallet) { | ||||
wallet = await getWalletDetails(existingWallet); | wallet = await getWalletDetails(existingWallet); | ||||
await localforage.setItem('wallet', wallet); | await localforage.setItem('wallet', wallet); | ||||
} else { | } else { | ||||
wallet = existingWallet; | wallet = existingWallet; | ||||
} | } | ||||
console.log(`wallet returned from getWallet()`, wallet); | |||||
return wallet; | return wallet; | ||||
}; | }; | ||||
const migrateLegacyWallet = async (BCH, wallet) => { | const migrateLegacyWallet = async (BCH, wallet) => { | ||||
console.log(`migrateLegacyWallet`); | console.log(`migrateLegacyWallet`); | ||||
console.log(`legacyWallet`, wallet); | console.log(`legacyWallet`, wallet); | ||||
const NETWORK = process.env.REACT_APP_NETWORK; | const NETWORK = process.env.REACT_APP_NETWORK; | ||||
const mnemonic = wallet.mnemonic; | const mnemonic = wallet.mnemonic; | ||||
▲ Show 20 Lines • Show All 682 Lines • ▼ Show 20 Lines | ) { | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// Update wallet every 10s | // Update wallet every 10s | ||||
useAsyncTimeout(async () => { | useAsyncTimeout(async () => { | ||||
const wallet = await getWallet(); | const wallet = await getWallet(); | ||||
update({ | chronikUpdate({ | ||||
wallet, | wallet, | ||||
}).finally(() => { | }).finally(() => { | ||||
setLoading(false); | setLoading(false); | ||||
if (!hasUpdated) { | if (!hasUpdated) { | ||||
setHasUpdated(true); | setHasUpdated(true); | ||||
} | } | ||||
}); | }); | ||||
}, 1000); | }, 10000); | ||||
const fetchBchPrice = async ( | const fetchBchPrice = async ( | ||||
fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd', | fiatCode = cashtabSettings ? cashtabSettings.fiatCurrency : 'usd', | ||||
) => { | ) => { | ||||
// Split this variable out in case coingecko changes | // Split this variable out in case coingecko changes | ||||
const cryptoId = currency.coingeckoId; | const cryptoId = currency.coingeckoId; | ||||
// Keep this in the code, because different URLs will have different outputs require different parsing | // Keep this in the code, because different URLs will have different outputs require different parsing | ||||
const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`; | const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`; | ||||
▲ Show 20 Lines • Show All 82 Lines • Show Last 20 Lines |
NB: this approach is not robust. It just happens to work for a wallet only receiving transactions at 1 or 2 addresses.
Prod implementation will need to be more sophisticated.