From ecc41bac8d0471ad623d6236e5a17de218487503 Mon Sep 17 00:00:00 2001 From: Aleksandr Kukharenko Date: Tue, 26 Mar 2024 14:33:50 +0200 Subject: [PATCH] feat: auto-close + indexer --- package.json | 2 +- src/plugins/contracts/api.js | 78 ---------------- src/plugins/contracts/events-listener.js | 98 -------------------- src/plugins/contracts/index.js | 112 ++++++++++------------- src/plugins/contracts/indexer.js | 70 ++++++++++++++ 5 files changed, 120 insertions(+), 240 deletions(-) delete mode 100644 src/plugins/contracts/events-listener.js create mode 100644 src/plugins/contracts/indexer.js diff --git a/package.json b/package.json index 2068d3d..a5b17af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lumerin/wallet-core", - "version": "1.0.90", + "version": "1.1.0", "author": { "name": "Lumerin", "email": "developer@lumerin.io", diff --git a/src/plugins/contracts/api.js b/src/plugins/contracts/api.js index 77f8dd9..0c4ce70 100644 --- a/src/plugins/contracts/api.js +++ b/src/plugins/contracts/api.js @@ -3,7 +3,6 @@ const logger = require('../../logger') const { encrypt } = require('ecies-geth') const { Implementation } = require('contracts-js') const { remove0xPrefix, add65BytesPrefix } = require('./helpers') -const { ContractEventsListener } = require('./events-listener') const ethereumWallet = require('ethereumjs-wallet').default /** @@ -104,78 +103,6 @@ async function _loadContractInstance( } } -/** - * @param {import('web3').default} web3 - * @param {import('web3').default} web3Subscriptionable - * @param {import('contracts-js').LumerinContext} lumerin - * @param {import('contracts-js').CloneFactoryContext} cloneFactory - * @param {string[]} addresses - * @param {string} walletAddress - */ -async function getContracts( - web3, - web3Subscriptionable, - lumerin, - cloneFactory, - addresses, - walletAddress, - eventBus -) { - const chunkSize = 5 - const result = [] - for (let i = 0; i < addresses.length; i += chunkSize) { - const contracts = await Promise.all( - addresses - .slice(i, i + chunkSize) - .map((address) => - getContract( - web3, - web3Subscriptionable, - lumerin, - cloneFactory, - address, - walletAddress - ) - ) - ) - eventBus.emit('contract-updated', { - actives: contracts, - }) - result.push(...contracts) - } - return result -} - -/** - * @param {import('web3').default} web3 - * @param {import('web3').default} web3Subscriptionable - * @param {import('contracts-js').LumerinContext} lumerin - * @param {string} contractId - * @param {string} walletAddress - */ -async function getContract( - web3, - web3Subscriptionable, - lumerin, - cloneFactory, - contractId, - walletAddress -) { - const contractEventsListener = ContractEventsListener.getInstance() - const contractInfo = await _loadContractInstance( - web3, - contractId, - walletAddress - ) - - contractEventsListener.addContract( - contractInfo.data.id, - Implementation(web3Subscriptionable, contractId), - walletAddress - ) - return contractInfo.data -} - /** * @param {import('contracts-js').CloneFactoryContext} cloneFactory */ @@ -329,9 +256,6 @@ function setContractDeleteStatus(web3, cloneFactory, onUpdate) { from: walletAddress, gas, }) - onUpdate(contractId, walletAddress).catch((err) => - logger.error(`Failed to refresh after setContractDeadStatus: ${err}`) - ) return result } } @@ -453,8 +377,6 @@ function editContract(web3, cloneFactory, lumerin) { } module.exports = { - getContracts, - getContract, createContract, cancelContract, purchaseContract, diff --git a/src/plugins/contracts/events-listener.js b/src/plugins/contracts/events-listener.js deleted file mode 100644 index 52b2e45..0000000 --- a/src/plugins/contracts/events-listener.js +++ /dev/null @@ -1,98 +0,0 @@ -//@ts-check -// const debug = require('debug')('lmr-wallet:core:contracts:event-listener') -const logger = require('../../logger'); - -class ContractEventsListener { - /** - * @param {import('contracts-js').CloneFactoryContext} cloneFactory - */ - constructor(cloneFactory) { - this.cloneFactory = cloneFactory - this.cloneFactoryListener = null - this.contracts = {} - this.walletAddress = null; - } - - /** - * @param {(contractId?: string, walletAddress?: string) => void} onUpdate - */ - setOnUpdate(onUpdate) { - this.onUpdate = onUpdate - } - - /** - * - * @param {string} id - * @param {import('contracts-js').ImplementationContext} instance - * @param {string} walletAddress - */ - addContract(id, instance, walletAddress) { - if (!this.contracts[id]) { - this.contracts[id] = instance.events.allEvents() - this.contracts[id] - .on('connected', () => { - logger.debug(`Start listen contract (${id}) events`) - }) - .on('data', async () => { - logger.debug(`Contract (${id}) updated`) - if (this.onUpdate){ - await new Promise((resolve) => setTimeout(resolve, 1000)) - this.onUpdate(id, this.walletAddress || walletAddress) - } - }) - } - } - - listenCloneFactory() { - if (!this.cloneFactoryListener) { - this.cloneFactoryListener = this.cloneFactory.events.contractCreated() - this.cloneFactoryListener - .on('connected', () => { - logger.debug('Start listen clone factory events') - }) - .on('data', async (event) => { - const contractId = event.returnValues._address - logger.debug('New contract created', contractId) - await new Promise((resolve) => setTimeout(resolve, 1000)) - this.onUpdate(contractId, this.walletAddress) - }) - } - } - - /** - * @static - * @param {import('contracts-js').CloneFactoryContext} cloneFactory - * @param {boolean} [debugEnabled=false] - * @returns {ContractEventsListener} - */ - static create(cloneFactory, debugEnabled = false) { - if (ContractEventsListener.instance) { - return ContractEventsListener.instance - } - - const instance = new ContractEventsListener(cloneFactory) - ContractEventsListener.instance = instance - instance.listenCloneFactory() - return instance - } - - /** - * @returns {ContractEventsListener} - */ - static getInstance() { - if (!ContractEventsListener.instance) { - throw new Error("ContractEventsListener instance not created") - } - return ContractEventsListener.instance - } - - /** - * @static - * @param {(contractId?: string) => void} onUpdate - */ - static setOnUpdate(onUpdate) { - ContractEventsListener.getInstance().onUpdate = onUpdate - } -} - -module.exports = { ContractEventsListener } diff --git a/src/plugins/contracts/index.js b/src/plugins/contracts/index.js index 4e1c4f1..c097a7e 100644 --- a/src/plugins/contracts/index.js +++ b/src/plugins/contracts/index.js @@ -1,26 +1,18 @@ //@ts-check 'use strict' -// const debug = require('debug')('lmr-wallet:core:contracts') -const logger = require('../../logger'); +const logger = require('../../logger') const { Lumerin, CloneFactory } = require('contracts-js') -/** - * @type {typeof import('web3').default} - */ -//@ts-ignore -const Web3 = require('web3') - const { - getContracts, createContract, cancelContract, purchaseContract, setContractDeleteStatus, editContract, - getMarketplaceFee + getMarketplaceFee, } = require('./api') -const { ContractEventsListener } = require('./events-listener') +const { Indexer } = require('./indexer') /** * Create a plugin instance. @@ -35,76 +27,70 @@ function createPlugin() { * @returns {{ api: {[key: string]:any}, events: string[], name: string }} The instance details. */ function start({ config, eventBus, plugins }) { - const { lmrTokenAddress, cloneFactoryAddress } = config + const { + lmrTokenAddress, + cloneFactoryAddress, + indexerUrl, + pollingInterval, + } = config const { eth } = plugins const web3 = eth.web3 - const web3Subscriptionable = new Web3(plugins.eth.web3SubscriptionProvider) const lumerin = Lumerin(web3, lmrTokenAddress) const cloneFactory = CloneFactory(web3, cloneFactoryAddress) - const cloneFactorySubscriptionable = CloneFactory( - web3Subscriptionable, - cloneFactoryAddress - ) - const refreshContracts = - (web3, lumerin, cloneFactory) => async (contractId, walletAddress) => { - eventBus.emit('contracts-scan-started', {}) - ContractEventsListener.getInstance().walletAddress = walletAddress; - const addresses = contractId - ? [contractId] - : await cloneFactory.methods - .getContractList() - .call() - .catch((error) => { - logger.error('cannot get list of contract addresses:', error) - throw error - }) + const indexer = new Indexer(indexerUrl) - return getContracts( - web3, - web3Subscriptionable, - lumerin, - cloneFactory, - addresses, - walletAddress, - eventBus, - ) - .then((contracts) => { - eventBus.emit('contracts-scan-finished', { - actives: contracts, - }) - }) - .catch(function (error) { - logger.error('Could not sync contracts/events', error) - throw error + const refreshContracts = async (contractId, walletAddress) => { + if (walletAddress) { + Indexer.walletAddr = walletAddress + } + eventBus.emit('contracts-scan-started', {}) + + try { + const contracts = contractId + ? await indexer.getContract(contractId) + : await indexer.getContracts() + + eventBus.emit('contracts-scan-finished', { + actives: contracts, }) + } catch (error) { + logger.error( + `Could not sync contracts/events, params: ${contractId}, error:`, + error + ) + throw error + } } - const contractEventsListener = ContractEventsListener.create( - cloneFactorySubscriptionable, - config.debug - ) + setInterval(() => { + refreshContracts() + }, pollingInterval) - const onUpdate = refreshContracts(web3, lumerin, cloneFactory) - contractEventsListener.setOnUpdate(onUpdate) + const wrapAction = (fn) => async (params) => { + const contractId = params?.contractId + const result = await fn(params) + await new Promise((resolve) => setTimeout(resolve, 1000)) + await refreshContracts(contractId).catch((error) => { + logger.error('Error refreshing contracts', error) + }) + return result + } - const refreshContractsFn = refreshContracts(web3, lumerin, cloneFactory) const purchaseContractFn = purchaseContract(web3, cloneFactory, lumerin) const cancelContractFn = cancelContract(web3, cloneFactory) return { api: { - refreshContracts: refreshContractsFn, - createContract: createContract(web3, cloneFactory), - cancelContract: cancelContractFn, - purchaseContract: purchaseContractFn, - editContract: editContract(web3, cloneFactory, lumerin), + refreshContracts, + createContract: wrapAction(createContract(web3, cloneFactory)), + cancelContract: wrapAction(cancelContractFn), + purchaseContract: wrapAction(purchaseContractFn), + editContract: wrapAction(editContract(web3, cloneFactory, lumerin)), getMarketplaceFee: getMarketplaceFee(cloneFactory), - setContractDeleteStatus: setContractDeleteStatus( - web3, - cloneFactory, - onUpdate, + setContractDeleteStatus: wrapAction( + setContractDeleteStatus(web3, cloneFactory) ), }, events: [ diff --git a/src/plugins/contracts/indexer.js b/src/plugins/contracts/indexer.js new file mode 100644 index 0000000..26f0430 --- /dev/null +++ b/src/plugins/contracts/indexer.js @@ -0,0 +1,70 @@ +const axios = require('axios').default + +class Indexer { + /** + * @type {string} + */ + static walletAddr = null + + /** + * + * @param {string} url + */ + constructor(url) { + this.url = url + + this.headers = { + 'Content-Type': 'application/json', + 'User-Agent': `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36`, + } + } + + /** + * + * @returns {Promise} + */ + getContracts = async () => { + const params = Indexer.walletAddr ? { walletAddr: Indexer.walletAddr } : {} + + const response = await axios.get(`${this.url}/api/contracts`, { + params, + headers: this.headers, + }) + return this.mapIndexerContracts(response.data) + } + + /** + * + * @param {string} id + * @returns {Promise} + */ + getContract = async (id) => { + const params = Indexer.walletAddr ? { walletAddr: Indexer.walletAddr } : {} + + const response = await axios.get(`${this.url}/api/contracts/${id}`, { + params, + headers: this.headers, + }) + return this.mapIndexerContracts([response.data]) + } + + /** + * @param {object[]} contracts + */ + mapIndexerContracts(contracts) { + return contracts.map((c) => { + return { + ...c, + isDead: c.isDeleted, + encryptedPoolData: c.encrValidatorUrl, + timestamp: c.startingBlockTimestamp, + history: c.history.map((h) => ({ + ...h, + id: c.id, + })), + } + }) + } +} + +module.exports = { Indexer }