-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #863 from nervosnetwork/impl-indexer
feat: sync by indexer RPCs
- Loading branch information
Showing
13 changed files
with
592 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 22 additions & 10 deletions
32
packages/neuron-wallet/src/database/chain/migrations/1562038960990-AddStatusToTx.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
packages/neuron-wallet/src/database/chain/migrations/1565693320664-AddConfirmed.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import {MigrationInterface, QueryRunner} from "typeorm"; | ||
|
||
export class AddConfirmed1565693320664 implements MigrationInterface { | ||
|
||
public async up(queryRunner: QueryRunner): Promise<any> { | ||
await queryRunner.query(`ALTER TABLE 'transaction' ADD COLUMN 'confirmed' boolean NOT NULL DEFAULT false;`) | ||
} | ||
|
||
public async down(queryRunner: QueryRunner): Promise<any> { | ||
await queryRunner.dropColumn('transaction', 'confirmed') | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
packages/neuron-wallet/src/services/indexer/indexer-rpc.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import Core from '@nervosnetwork/ckb-sdk-core' | ||
|
||
export default class IndexerRPC { | ||
private core: Core | ||
|
||
constructor(url: string) { | ||
this.core = new Core(url) | ||
} | ||
|
||
public deindexLockHash = async (lockHash: string) => { | ||
return this.core.rpc.deindexLockHash(lockHash) | ||
} | ||
|
||
public indexLockHash = async (lockHash: string, indexFrom = '0') => { | ||
return this.core.rpc.indexLockHash(lockHash, indexFrom) | ||
} | ||
|
||
public getTransactionByLockHash = async ( | ||
lockHash: string, | ||
page: string, | ||
per: string, | ||
reverseOrder: boolean = false | ||
) => { | ||
const result = await this.core.rpc.getTransactionsByLockHash(lockHash, page, per, reverseOrder) | ||
return result | ||
} | ||
|
||
public getLockHashIndexStates = async () => { | ||
return this.core.rpc.getLockHashIndexStates() | ||
} | ||
|
||
public getLiveCellsByLockHash = async ( | ||
lockHash: string, | ||
page: string, | ||
per: string, | ||
reverseOrder: boolean = false | ||
) => { | ||
const result = await this.core.rpc.getLiveCellsByLockHash(lockHash, page, per, reverseOrder) | ||
return result | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
import { Subject, Subscription } from 'rxjs' | ||
import Utils from 'services/sync/utils' | ||
import logger from 'utils/logger' | ||
import GetBlocks from 'services/sync/get-blocks' | ||
import { Transaction } from 'types/cell-types' | ||
import TypeConvert from 'types/type-convert' | ||
import BlockNumber from 'services/sync/block-number' | ||
import AddressesUsedSubject from 'models/subjects/addresses-used-subject' | ||
import LockUtils from 'models/lock-utils' | ||
import TransactionPersistor from 'services/tx/transaction-persistor' | ||
import IndexerTransaction from 'services/tx/indexer-transaction' | ||
|
||
import IndexerRPC from './indexer-rpc' | ||
|
||
enum TxPointType { | ||
CreatedBy = 'createdBy', | ||
ConsumedBy = 'consumedBy', | ||
} | ||
|
||
export default class IndexerQueue { | ||
private lockHashes: string[] | ||
private indexerRPC: IndexerRPC | ||
private getBlocksService: GetBlocks | ||
private per = 50 | ||
private interval = 1000 | ||
private blockNumberService: BlockNumber | ||
private tipNumberListener: Subscription | ||
private tipBlockNumber: bigint = BigInt(-1) | ||
|
||
private stopped = false | ||
private indexed = false | ||
|
||
private inProcess = false | ||
|
||
private resetFlag = false | ||
|
||
constructor(url: string, lockHashes: string[], tipNumberSubject: Subject<string | undefined>) { | ||
this.lockHashes = lockHashes | ||
this.indexerRPC = new IndexerRPC(url) | ||
this.getBlocksService = new GetBlocks() | ||
this.blockNumberService = new BlockNumber() | ||
this.tipNumberListener = tipNumberSubject.subscribe(async (num: string) => { | ||
if (num) { | ||
this.tipBlockNumber = BigInt(num) | ||
} | ||
}) | ||
} | ||
|
||
public setLockHashes = (lockHashes: string[]): void => { | ||
this.lockHashes = lockHashes | ||
this.indexed = false | ||
} | ||
|
||
public reset = () => { | ||
this.resetFlag = true | ||
} | ||
|
||
/* eslint no-await-in-loop: "off" */ | ||
/* eslint no-restricted-syntax: "off" */ | ||
public start = async () => { | ||
while (!this.stopped) { | ||
try { | ||
this.inProcess = true | ||
if (this.resetFlag) { | ||
await this.blockNumberService.updateCurrent(BigInt(0)) | ||
this.resetFlag = false | ||
} | ||
const { lockHashes } = this | ||
const currentBlockNumber: bigint = await this.blockNumberService.getCurrent() | ||
if (!this.indexed || currentBlockNumber !== this.tipBlockNumber) { | ||
if (!this.indexed) { | ||
await this.indexLockHashes(lockHashes) | ||
this.indexed = true | ||
} | ||
const minBlockNumber = await this.getCurrentBlockNumber(lockHashes) | ||
for (const lockHash of lockHashes) { | ||
await this.pipeline(lockHash, TxPointType.CreatedBy, currentBlockNumber) | ||
} | ||
for (const lockHash of lockHashes) { | ||
await this.pipeline(lockHash, TxPointType.ConsumedBy, currentBlockNumber) | ||
} | ||
if (minBlockNumber) { | ||
await this.blockNumberService.updateCurrent(minBlockNumber) | ||
} | ||
} | ||
await this.yield(this.interval) | ||
} catch (err) { | ||
if (err.message.startsWith('connect ECONNREFUSED')) { | ||
logger.debug('sync indexer error:', err) | ||
} else { | ||
logger.error('sync indexer error:', err) | ||
} | ||
} finally { | ||
await this.yield() | ||
this.inProcess = false | ||
} | ||
} | ||
} | ||
|
||
public processFork = async () => { | ||
while (!this.stopped) { | ||
try { | ||
const tip = this.tipBlockNumber | ||
const txs = await IndexerTransaction.txHashes() | ||
for (const tx of txs) { | ||
const result = await this.getBlocksService.getTransaction(tx.hash) | ||
if (!result) { | ||
await IndexerTransaction.deleteTxWhenFork(tx.hash) | ||
} else if (tip - BigInt(tx.blockNumber) >= 1000) { | ||
await IndexerTransaction.confirm(tx.hash) | ||
} | ||
} | ||
} catch (err) { | ||
logger.error(`indexer delete forked tx:`, err) | ||
} finally { | ||
await this.yield(10000) | ||
} | ||
} | ||
} | ||
|
||
public getCurrentBlockNumber = async (lockHashes: string[]) => { | ||
// get lock hash indexer status | ||
const lockHashIndexStates = await this.indexerRPC.getLockHashIndexStates() | ||
const blockNumbers = lockHashIndexStates | ||
.filter(state => lockHashes.includes(state.lockHash)) | ||
.map(state => state.blockNumber) | ||
const uniqueBlockNumbers = [...new Set(blockNumbers)] | ||
const blockNumbersBigInt = uniqueBlockNumbers.map(num => BigInt(num)) | ||
const minBlockNumber = Utils.min(blockNumbersBigInt) | ||
return minBlockNumber | ||
} | ||
|
||
public indexLockHashes = async (lockHashes: string[]) => { | ||
const lockHashIndexStates = await this.indexerRPC.getLockHashIndexStates() | ||
const indexedLockHashes: string[] = lockHashIndexStates.map(state => state.lockHash) | ||
const nonIndexedLockHashes = lockHashes.filter(i => !indexedLockHashes.includes(i)) | ||
|
||
await Utils.mapSeries(nonIndexedLockHashes, async (lockHash: string) => { | ||
await this.indexerRPC.indexLockHash(lockHash) | ||
}) | ||
} | ||
|
||
// type: 'createdBy' | 'consumedBy' | ||
public pipeline = async (lockHash: string, type: TxPointType, startBlockNumber: bigint) => { | ||
let page = 0 | ||
let stopped = false | ||
while (!stopped) { | ||
const txs = await this.indexerRPC.getTransactionByLockHash(lockHash, page.toString(), this.per.toString()) | ||
if (txs.length < this.per) { | ||
stopped = true | ||
} | ||
for (const tx of txs) { | ||
let txPoint: CKBComponents.TransactionPoint | null = null | ||
if (type === TxPointType.CreatedBy) { | ||
txPoint = tx.createdBy | ||
} else if (type === TxPointType.ConsumedBy) { | ||
txPoint = tx.consumedBy | ||
} | ||
|
||
if ( | ||
txPoint && | ||
(BigInt(txPoint.blockNumber) >= startBlockNumber || this.tipBlockNumber - BigInt(txPoint.blockNumber) < 1000) | ||
) { | ||
const transactionWithStatus = await this.getBlocksService.getTransaction(txPoint.txHash) | ||
const ckbTransaction: CKBComponents.Transaction = transactionWithStatus.transaction | ||
const transaction: Transaction = TypeConvert.toTransaction(ckbTransaction) | ||
// tx timestamp / blockNumber / blockHash | ||
const { blockHash } = transactionWithStatus.txStatus | ||
if (blockHash) { | ||
const blockHeader = await this.getBlocksService.getHeader(blockHash) | ||
transaction.blockHash = blockHash | ||
transaction.blockNumber = blockHeader.number | ||
transaction.timestamp = blockHeader.timestamp | ||
} | ||
// broadcast address used | ||
const txEntity = await TransactionPersistor.saveFetchTx(transaction) | ||
|
||
let address: string | undefined | ||
if (type === TxPointType.CreatedBy) { | ||
address = LockUtils.lockScriptToAddress(transaction.outputs![+txPoint.index].lock) | ||
} else if (type === TxPointType.ConsumedBy) { | ||
const input = txEntity.inputs[+txPoint.index] | ||
const output = await IndexerTransaction.updateInputLockHash(input.outPointTxHash!, input.outPointIndex!) | ||
if (output) { | ||
address = LockUtils.lockScriptToAddress(output.lock) | ||
} | ||
} | ||
if (address) { | ||
AddressesUsedSubject.getSubject().next([address]) | ||
} | ||
} | ||
} | ||
page += 1 | ||
} | ||
} | ||
|
||
public stop = () => { | ||
this.tipNumberListener.unsubscribe() | ||
this.stopped = true | ||
} | ||
|
||
public waitForDrained = async (timeout: number = 5000) => { | ||
const startAt: number = +new Date() | ||
while (this.inProcess) { | ||
const now: number = +new Date() | ||
if (now - startAt > timeout) { | ||
return | ||
} | ||
await this.yield(50) | ||
} | ||
} | ||
|
||
public stopAndWait = async () => { | ||
this.stop() | ||
await this.waitForDrained() | ||
} | ||
|
||
private yield = async (millisecond: number = 1) => { | ||
await Utils.sleep(millisecond) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.