Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate Edge Change Server #908

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"require": "sucrase/register",
"spec": "test/**/*.ts"
}
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- changed: Implement use of edge-change-server for network syncing.

## 4.41.0 (2025-03-14)

- changed: (FIO) Require apiToken for all calls to `buyAddressRequest`
Expand All @@ -12,7 +14,7 @@

## 4.39.0 (2025-03-10)

- added: (ALGO) Throw if recipient did not activate the token being sent in `signTx`
- added: (ALGO) Throw if recipient did not activate the token being sent in `signTx`

## 4.38.0 (2025-03-03)

Expand Down
1 change: 1 addition & 0 deletions cli/cliContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export async function makeCliEngine(
onNewTokens: () => log('onNewTokens'),
onSeenTxCheckpoint: () => log('onSeenTxCheckpoint'),
onStakingStatusChanged: () => log('onStakingStatusChanged'),
onSubscribeAddresses: () => log('onSubscribeAddresses'),
onTokenBalanceChanged: () => log('onTokenBalanceChanged'),
onTransactions: () => log('onTransactionsChanged'),
onTransactionsChanged: () => log('onTransactionsChanged'),
Expand Down
46 changes: 46 additions & 0 deletions src/ethereum/EthereumEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
EdgeCurrencyEngine,
EdgeCurrencyEngineOptions,
EdgeCurrencyInfo,
EdgeEngineSyncNetworkOptions,
EdgeFetchFunction,
EdgeFreshAddress,
EdgeSignMessageOptions,
Expand Down Expand Up @@ -37,6 +38,7 @@ import {
mergeDeeply,
normalizeAddress,
removeHexPrefix,
snooze,
toHex,
uint8ArrayToHex
} from '../common/utils'
Expand Down Expand Up @@ -90,6 +92,11 @@ import {
} from './fees/feeProviders'
import { RpcAdapter } from './networkAdapters/RpcAdapter'

// How long to wait before retrying a network sync if the blockheight from
// our querying hasn't exceeded the blockheight from the core:
const RETRY_SYNC_NETWORK_INTERVAL = 2000
const SYNC_NETWORK_INTERVAL = 20000

export class EthereumEngine extends CurrencyEngine<
EthereumTools,
SafeEthWalletInfo
Expand Down Expand Up @@ -762,6 +769,10 @@ export class EthereumEngine extends CurrencyEngine<
this.addToLoop('updateOptimismRollupParams', ROLLUP_FEE_PARAMS)
this.ethNetwork.start()
await super.startEngine()

this.currencyEngineCallbacks.onSubscribeAddresses([
this.walletLocalData.publicKey
])
}

async killEngine(): Promise<void> {
Expand All @@ -775,6 +786,41 @@ export class EthereumEngine extends CurrencyEngine<
await this.startEngine()
}

async syncNetwork(opts: EdgeEngineSyncNetworkOptions): Promise<number> {
const { subscribeParam } = opts

// Do a regular routine sync:
if (subscribeParam == null) {
await this.ethNetwork.acquireUpdates()
return SYNC_NETWORK_INTERVAL
}
// The blockheight from the network (change server)
const theirBlockheight =
subscribeParam.checkpoint != null
? parseInt(subscribeParam.checkpoint)
: undefined

// Sync an update that is from the mempool:
if (theirBlockheight == null) {
// Wait to let the update propagate:
await snooze(10000)
// TODO: Continuously aquire updates until a change is detected for a
// specific address.
await this.ethNetwork.acquireUpdates()
return SYNC_NETWORK_INTERVAL
}

// Sync the network until the engine blockheight matches the checkpoint:
do {
await this.ethNetwork.acquireUpdates()
} while (
theirBlockheight > this.walletLocalData.blockHeight &&
(await snooze(RETRY_SYNC_NETWORK_INTERVAL)) == null
)

return SYNC_NETWORK_INTERVAL
}

async getFreshAddress(): Promise<EdgeFreshAddress> {
const { publicKey } = this.walletLocalData
const publicAddress = /[A-F]/.test(publicKey)
Expand Down
182 changes: 63 additions & 119 deletions src/ethereum/EthereumNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import {
const BLOCKHEIGHT_POLL_MILLISECONDS = getRandomDelayMs(20000)
const NEEDS_LOOP_INTERVAL = 1000
const NONCE_POLL_MILLISECONDS = getRandomDelayMs(20000)
const BAL_POLL_MILLISECONDS = getRandomDelayMs(20000)
const TXS_POLL_MILLISECONDS = getRandomDelayMs(20000)

const ADDRESS_QUERY_LOOKBACK_SEC = 2 * 60 // ~ 2 minutes

Expand Down Expand Up @@ -278,6 +276,7 @@ export class EthereumNetwork {
stop(): void {
this.needsLoopTask.stop()
this.disconnectNetworkAdapters()
// TODO: Abort all in-flight network sync requests
}

acquireBlockHeight = makeThrottledFunction(
Expand All @@ -296,77 +295,64 @@ export class EthereumNetwork {
}
)

acquireTokenBalance = makeKeyBasedThrottledFunction(
BAL_POLL_MILLISECONDS,
async (currencyCode: string): Promise<void> => {
const update = await this.check('fetchTokenBalance', currencyCode)
return this.processEthereumNetworkUpdate(update)
}
)
acquireTokenBalance = async (currencyCode: string): Promise<void> => {
const update = await this.check('fetchTokenBalance', currencyCode)
return this.processEthereumNetworkUpdate(update)
}

acquireTokenBalances = makeThrottledFunction(
BAL_POLL_MILLISECONDS,
async (): Promise<void> => {
const update = await this.check('fetchTokenBalances')
return this.processEthereumNetworkUpdate(update)
}
)
acquireTokenBalances = async (): Promise<void> => {
const update = await this.check('fetchTokenBalances')
return this.processEthereumNetworkUpdate(update)
}

acquireTxs = makeKeyBasedThrottledFunction(
TXS_POLL_MILLISECONDS,
async (currencyCode: string): Promise<void> => {
const lastTransactionQueryHeight =
this.ethEngine.walletLocalData.lastTransactionQueryHeight[
currencyCode
] ?? 0
const lastTransactionDate =
this.ethEngine.walletLocalData.lastTransactionDate[currencyCode] ?? 0
const addressQueryLookbackBlocks =
this.ethEngine.networkInfo.addressQueryLookbackBlocks
const params = {
// Only query for transactions as far back as ADDRESS_QUERY_LOOKBACK_BLOCKS from the last time we queried transactions
startBlock: Math.max(
lastTransactionQueryHeight - addressQueryLookbackBlocks,
0
),
// Only query for transactions as far back as ADDRESS_QUERY_LOOKBACK_SEC from the last time we queried transactions
startDate: Math.max(
lastTransactionDate - ADDRESS_QUERY_LOOKBACK_SEC,
0
),
currencyCode
}
acquireTxs = async (currencyCode: string): Promise<void> => {
const lastTransactionQueryHeight =
this.ethEngine.walletLocalData.lastTransactionQueryHeight[currencyCode] ??
0
const lastTransactionDate =
this.ethEngine.walletLocalData.lastTransactionDate[currencyCode] ?? 0
const addressQueryLookbackBlocks =
this.ethEngine.networkInfo.addressQueryLookbackBlocks
const params = {
// Only query for transactions as far back as ADDRESS_QUERY_LOOKBACK_BLOCKS from the last time we queried transactions
startBlock: Math.max(
lastTransactionQueryHeight - addressQueryLookbackBlocks,
0
),
// Only query for transactions as far back as ADDRESS_QUERY_LOOKBACK_SEC from the last time we queried transactions
startDate: Math.max(lastTransactionDate - ADDRESS_QUERY_LOOKBACK_SEC, 0),
currencyCode
}

// Send an empty tokenTxs network update if no network adapters
// qualify for 'fetchTxs':
if (
this.qualifyNetworkAdapters('fetchTxs').length === 0 ||
this.ethEngine.lightMode
) {
const tokenTxs: {
[currencyCode: string]: EdgeTransactionsBlockHeightTuple
} = {
[this.ethEngine.currencyInfo.currencyCode]: {
blockHeight: params.startBlock,
edgeTransactions: []
}
// Send an empty tokenTxs network update if no network adapters
// qualify for 'fetchTxs':
if (
this.qualifyNetworkAdapters('fetchTxs').length === 0 ||
this.ethEngine.lightMode
) {
const tokenTxs: {
[currencyCode: string]: EdgeTransactionsBlockHeightTuple
} = {
[this.ethEngine.currencyInfo.currencyCode]: {
blockHeight: params.startBlock,
edgeTransactions: []
}
for (const token of Object.values(this.ethEngine.allTokensMap)) {
tokenTxs[token.currencyCode] = {
blockHeight: params.startBlock,
edgeTransactions: []
}
}
for (const token of Object.values(this.ethEngine.allTokensMap)) {
tokenTxs[token.currencyCode] = {
blockHeight: params.startBlock,
edgeTransactions: []
}
return this.processEthereumNetworkUpdate({
tokenTxs,
server: 'none'
})
}

const update = await this.check('fetchTxs', params)
return this.processEthereumNetworkUpdate(update)
return this.processEthereumNetworkUpdate({
tokenTxs,
server: 'none'
})
}
)

const update = await this.check('fetchTxs', params)
return this.processEthereumNetworkUpdate(update)
}

needsLoop = async (): Promise<void> => {
this.acquireBlockHeight().catch(error => {
Expand All @@ -378,7 +364,12 @@ export class EthereumNetwork {
console.error(error)
this.ethEngine.error('needsLoop acquireNonce', error)
})
}

/**
* This function gets the balance and transaction updates from the network.
*/
acquireUpdates = async (): Promise<void> => {
// The engine supports token balances batch queries if an adapter provides
// the functionality.
const isFetchTokenBalancesSupported =
Expand All @@ -387,8 +378,6 @@ export class EthereumNetwork {
) != null

if (
// Only check if the engine needs balances.
this.shouldCheckAndUpdateTxsOrBalances() &&
// If this engine supports the batch token balance query, no need to check
// each currencyCode individually.
isFetchTokenBalancesSupported
Expand All @@ -408,24 +397,20 @@ export class EthereumNetwork {

for (const currencyCode of currencyCodes) {
if (
// Only check if the engine needs balances.
this.shouldCheckAndUpdateTxsOrBalances() &&
// Only check each code individually if this engine does not support
// batch token balance queries.
!isFetchTokenBalancesSupported
) {
this.acquireTokenBalance(currencyCode)(currencyCode).catch(error => {
this.acquireTokenBalance(currencyCode).catch(error => {
console.error(error)
this.ethEngine.error('needsLoop acquireTokenBalance', error)
})
}

if (this.shouldCheckAndUpdateTxsOrBalances()) {
this.acquireTxs(currencyCode)(currencyCode).catch(error => {
console.error(error)
this.ethEngine.error('needsLoop acquireTxs', error)
})
}
this.acquireTxs(currencyCode).catch(error => {
console.error(error)
this.ethEngine.error('needsLoop acquireTxs', error)
})
}
}

Expand All @@ -435,33 +420,6 @@ export class EthereumNetwork {
)
}

protected shouldCheckAndUpdateTxsOrBalances(): boolean {
if (!this.ethEngine.addressesChecked) {
// The wallet is still doing initial sync
return true
}

if (this.isAnAdapterConnected()) {
// Conditions only while the wallet is connected to a network adapter

if (this.ethNeeds.addressSync.needsInitialSync) {
// The wallet has no push-based syndication state:
return true
}

if (this.ethNeeds.addressSync.needsTxids.length > 0) {
// The wallet has txs that need to be checked:
return true
}

// The wallet has no checks needed:
return false
}

// The default is always to check
return true
}

processEthereumNetworkUpdate = (
ethereumNetworkUpdate: EthereumNetworkUpdate
): void => {
Expand All @@ -479,10 +437,7 @@ export class EthereumNetwork {
)
const blockHeight = ethereumNetworkUpdate.blockHeight
this.ethEngine.log(`Got block height ${blockHeight}`)
if (
typeof blockHeight === 'number' &&
this.ethEngine.walletLocalData.blockHeight !== blockHeight
) {
if (this.ethEngine.walletLocalData.blockHeight !== blockHeight) {
this.ethEngine.checkDroppedTransactionsThrottled()
this.ethEngine.walletLocalData.blockHeight = blockHeight // Convert to decimal
this.ethEngine.walletLocalDataDirty = true
Expand Down Expand Up @@ -675,14 +630,3 @@ function makeThrottledFunction<Args extends any[], Rtn>(
})
}
}

function makeKeyBasedThrottledFunction<Args extends any[], Rtn>(
timeGap: number,
fn: (...args: Args) => Promise<Rtn>
): (key: string) => (...args: Args) => Promise<Rtn> {
const fns: { [key: string]: () => Promise<Rtn> } = {}
return (key: string) => {
fns[key] = fns[key] ?? makeThrottledFunction(timeGap, fn)
return fns[key]
}
}
Loading