diff --git a/package.json b/package.json index 03def86..4f9bbfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lumerin/wallet-core", - "version": "1.1.2", + "version": "1.1.4", "author": { "name": "Lumerin", "email": "developer@lumerin.io", @@ -36,8 +36,7 @@ "axios-cookiejar-support": "1.0.1", "bottleneck": "^2.19.5", "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.12", - "contracts-js": "github:Lumerin-protocol/contracts-js#v0.1.0", + "contracts-js": "github:Lumerin-protocol/contracts-js#v1.2.1", "cross-port-killer": "^1.4.0", "debug": "4.1.1", "ecies-geth": "^1.7.0", @@ -58,6 +57,7 @@ "websocket-reconnector": "1.1.1" }, "devDependencies": { + "@dopex-io/web3-multicall": "^0.1.10", "chai": "4.3.4", "chai-as-promised": "7.1.1", "check-tag-matches": "1.0.0", @@ -84,4 +84,4 @@ "engines": { "node": ">=12" } -} +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 1b2de0d..e1a85d7 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ const pluginCreators = [ require('./plugins/proxy-router'), require('./plugins/contracts'), require('./plugins/devices'), + require('./plugins/validator-registry') ] function createCore() { diff --git a/src/plugins/contracts/api.js b/src/plugins/contracts/api.js index 05f94a0..7de605d 100644 --- a/src/plugins/contracts/api.js +++ b/src/plugins/contracts/api.js @@ -3,8 +3,11 @@ const logger = require('../../logger') const { encrypt } = require('ecies-geth') const { Implementation } = require('contracts-js') const { remove0xPrefix, add65BytesPrefix } = require('./helpers') +const { decompressPublicKey } = require('../validator-registry/api') const ethereumWallet = require('ethereumjs-wallet').default +const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + /** * @param {import('web3').default} web3 * @param {string} implementationAddress @@ -45,7 +48,7 @@ async function _loadContractInstance( _length: length, // duration of the contract in seconds _version: version, _profitTarget: profitTarget - }, + }, _startingBlockTimestamp: timestamp, // timestamp of the block at moment of purchase _buyer: buyer, // wallet address of the purchasing party _seller: seller, // wallet address of the selling party @@ -253,73 +256,76 @@ function setContractDeleteStatus(web3, cloneFactory, onUpdate) { } } + /** * * @param {import('web3').default} web3 * @param {import('contracts-js').CloneFactoryContext} cloneFactory * @param {import('contracts-js').LumerinContext} lumerin - * @returns + * @returns {(p: {validatorUrl: string, validatorAddr: string, destUrl: string, validatorPubKeyYparity: boolean, validatorPubKeyX: `0x${string}`, contractAddr: string, price: string,version: number, privateKey: string}) => Promise} */ function purchaseContract(web3, cloneFactory, lumerin) { return async (params) => { - const { walletId, contractId, url, privateKey, price, version } = params - const sendOptions = { from: walletId } + const { validatorUrl, destUrl, validatorAddr, validatorPubKeyYparity, validatorPubKeyX, privateKey, contractAddr, price, version } = params; - //getting pubkey from contract to be purchased - const implementationContract = Implementation(web3, contractId) - - const pubKey = await implementationContract.methods.pubKey().call() - - //encrypting plaintext url parameter - const ciphertext = await encrypt( - Buffer.from(add65BytesPrefix(pubKey), 'hex'), - Buffer.from(url) - ) const account = web3.eth.accounts.privateKeyToAccount(privateKey) web3.eth.accounts.wallet.create(0).add(account) - const { - data: { isDead, price: p }, - } = await _loadContractInstance(web3, contractId) - if (isDead) { - throw new Error('Contract is deleted already') - } + const implementationContract = Implementation(web3, contractAddr) + const sellerPubKey = await implementationContract.methods.pubKey().call() + + const isThirdPartyValidator = validatorAddr !== ZERO_ADDRESS; + const encrDestPubKey = isThirdPartyValidator ? decompressPublicKey(validatorPubKeyYparity, validatorPubKeyX) : sellerPubKey; + + const encrValidatorUrl = await encrypt( + Buffer.from(add65BytesPrefix(sellerPubKey), 'hex'), + Buffer.from(validatorUrl) + ).then(res => res.toString('hex')) + + const encrDestUrl = await encrypt( + Buffer.from(add65BytesPrefix(encrDestPubKey), 'hex'), + Buffer.from(destUrl) + ).then(res => res.toString('hex')) const increaseAllowanceEstimate = await lumerin.methods .increaseAllowance(cloneFactory.options.address, price) .estimateGas({ - from: walletId, + from: account.address, }) await lumerin.methods .increaseAllowance(cloneFactory.options.address, price) .send({ - from: walletId, + from: account.address, gas: increaseAllowanceEstimate, }) const marketplaceFee = await cloneFactory.methods.marketplaceFee().call() const purchaseGas = await cloneFactory.methods - .setPurchaseRentalContract( - contractId, - ciphertext.toString('hex'), + .setPurchaseRentalContractV2( + contractAddr, + validatorAddr, + encrValidatorUrl, + encrDestUrl, version ) .estimateGas({ - from: sendOptions.from, + from: account.address, value: marketplaceFee, }) const purchaseResult = await cloneFactory.methods - .setPurchaseRentalContract( - contractId, - ciphertext.toString('hex'), + .setPurchaseRentalContractV2( + contractAddr, + validatorAddr, + encrValidatorUrl, + encrDestUrl, version ) .send({ - ...sendOptions, + from: account.address, gas: purchaseGas, value: marketplaceFee, }) @@ -357,7 +363,7 @@ function editContract(web3, cloneFactory, lumerin) { .estimateGas({ from: sendOptions.from, }); - + const editResult = await cloneFactory.methods .setUpdateContractInformationV2(contractId, price, limit, speed, duration, +profit) .send({ diff --git a/src/plugins/contracts/helpers.js b/src/plugins/contracts/helpers.js index a30e43a..7c08337 100644 --- a/src/plugins/contracts/helpers.js +++ b/src/plugins/contracts/helpers.js @@ -3,7 +3,17 @@ const remove0xPrefix = privateKey => privateKey.replace('0x', ''); // https://superuser.com/a/1465498 -const add65BytesPrefix = key => `04${key}`; +/** @param {string} key */ +const add65BytesPrefix = key => { + key = key.replace("0x", "") + + // 64 bytes hex string (2 char per byte) + if (key.length === 64 * 2) { + return `04${key}` + } + + return key +} module.exports = { remove0xPrefix, diff --git a/src/plugins/devices/configuration-strategies/ant-miner-strategy.js b/src/plugins/devices/configuration-strategies/ant-miner-strategy.js index 256ab2b..bbdbcc1 100644 --- a/src/plugins/devices/configuration-strategies/ant-miner-strategy.js +++ b/src/plugins/devices/configuration-strategies/ant-miner-strategy.js @@ -1,5 +1,5 @@ const AxiosDigestAuth = require('@mhoc/axios-digest-auth') -const cheerio = require('cheerio') +// const cheerio = require('cheerio') const { ConfigurationStrategyInterface } = require('./strategy.interface') diff --git a/src/plugins/devices/configuration-strategies/factory.js b/src/plugins/devices/configuration-strategies/factory.js index 15e1a70..9fb2b44 100644 --- a/src/plugins/devices/configuration-strategies/factory.js +++ b/src/plugins/devices/configuration-strategies/factory.js @@ -1,6 +1,6 @@ const { AbortSignal } = require('@azure/abort-controller') -const { AntMinerStrategy } = require('./ant-miner-strategy') +// const { AntMinerStrategy } = require('./ant-miner-strategy') const { TcpConfigurationStrategy } = require('./tcp-strategy') const { ConfigurationStrategyInterface } = require('./strategy.interface'); @@ -11,7 +11,7 @@ class ConfigurationStrategyFactory { * @returns {Promise} */ static async createStrategy(host, abort) { - const strategies = [TcpConfigurationStrategy, AntMinerStrategy] + const strategies = [TcpConfigurationStrategy] for (const Strategy of strategies) { try { const strategy = new Strategy(host, abort) diff --git a/src/plugins/eth/web3Http.js b/src/plugins/eth/web3Http.js index 4860d19..046b0d2 100644 --- a/src/plugins/eth/web3Http.js +++ b/src/plugins/eth/web3Http.js @@ -9,10 +9,16 @@ const isRateLimitError = (response, payload) => { return true } - if (payload?.method === 'eth_call' && (response.error?.message?.includes('execution reverted') || response.error?.code === -32000)) { + // Some providers return execution reverted error for eth_call when rate limiting + if ( + payload?.method === 'eth_call' && + response.error?.message?.includes('execution reverted') && + (response.error?.data === '' || response.error?.data === '0x' || response.error?.data === null || response.error?.data === undefined) + ) { return true } + const message = response.error?.message?.toLowerCase() if (!message) { return false @@ -21,9 +27,9 @@ const isRateLimitError = (response, payload) => { message.includes('too many requests') || message.includes('rate limit exceeded') || message.includes('reached maximum qps limit') || - message.includes('rate limit reached') || + message.includes('rate limit reached') || message.includes("we can't execute this request") || - message.includes("max message response size exceed") || + message.includes("max message response size exceed") || message.includes("upgrade your plan") || message.includes("Failed to validate quota usage") ); diff --git a/src/plugins/validator-registry/api.js b/src/plugins/validator-registry/api.js new file mode 100644 index 0000000..c3248f7 --- /dev/null +++ b/src/plugins/validator-registry/api.js @@ -0,0 +1,210 @@ +//@ts-check + +const ethereumWallet = require('ethereumjs-wallet').default +const { remove0xPrefix, add65BytesPrefix } = require('../contracts/helpers') +const { secp256k1 } = require('@noble/curves/secp256k1'); +const { hexToBytes, bytesToHex } = require('@noble/curves/abstract/utils'); +const { keccak256 } = require('web3-utils'); +/** @type {typeof import("@dopex-io/web3-multicall").default} */ +const Multicall = require("@dopex-io/web3-multicall"); + +// Cross-chain multicall address +const MulticallAddress = "0xcA11bde05977b3631167028862bE2a173976CA11" + +/** + * @typedef {Object} Validator + * @property {string} stake + * @property {string} addr + * @property {string} pubKeyYparity + * @property {string} lastComplainer + * @property {string} complains + * @property {string} host + * @property {string} pubKeyX + */ + +/** + * @param {import('contracts-js').ValidatorRegistryContext} registry + * @param {import('web3').default} web3 + * @returns {function(number, number): Promise>} Function that returns all active validators + */ +const getValidators = (registry, web3, chainId) => async (offset = 0, limit = 100) => { + const addresses = await registry.methods.getActiveValidators(String(offset), limit).call() + + const multicall = new Multicall({ provider: web3.currentProvider, multicallAddress: MulticallAddress }) + const result = await multicall.aggregate( + addresses.map(addr => registry.methods.getValidator(addr)) + ) + + return mapValidators(result); +} + +/** + * @param {import('contracts-js').ValidatorRegistryContext} registry + * @returns {function(string): Promise} Returns null if validator is not found + */ +const getValidator = (registry) => async (walletAddr) => { + return await _loadValidator(registry, walletAddr) +} + +/** @param {import('contracts-js').ValidatorRegistryContext} registry */ +const getValidatorsMinimalStake = (registry) => async () => { + return await registry.methods.stakeMinimum().call() +} + +/** @param {import('contracts-js').ValidatorRegistryContext} registry */ +const getValidatorsRegisterStake = (registry) => async () => { + return await registry.methods.stakeRegister().call() +} + +/** + * @param {import('contracts-js').ValidatorRegistryContext} registry + * @param {string} id + * @returns {Promise} + */ +const _loadValidator = async (registry, id) => { + const data = await registry.methods.getValidator(id).call().catch(err => { + if (err.data === getHash("ValidatorNotFound()")) { + return null + } + throw err + }); + + if (!data) { + return null + } + + return mapValidator(data) +} + +/** + * @param {import('contracts-js/dist/generated-types/ValidatorRegistry').ValidatorResponse[]} validators + * @returns {Validator[]} + */ +const mapValidators = (validators) => { + return validators.map(mapValidator) +} + +/** + * @param {import('contracts-js/dist/generated-types/ValidatorRegistry').ValidatorResponse} validator + * @returns {Validator} + */ +const mapValidator = (validator) => { + return { + stake: validator[0], + addr: validator[1], + pubKeyYparity: validator[2], + lastComplainer: validator[3], + complains: validator[4], + host: validator[5], + pubKeyX: validator[6] + } +} + +/** + * @param {import('contracts-js').ValidatorRegistryContext} registry + * @param {import('web3').default} web3 + */ +const deregisterValidator = (registry, web3) => async ({ walletId, privateKey }) => { + const account = web3.eth.accounts.privateKeyToAccount(privateKey) + web3.eth.accounts.wallet.create(0).add(account) + + const estimatedGas = await registry.methods + .validatorDeregister() + .estimateGas({ from: walletId }); + + await registry.methods.validatorDeregister().send({ from: walletId, gas: estimatedGas }); +} + +/** + * @typedef {Object} RegisterValidatorRequest + * @property {string} privateKey + * @property {string} stake + * @property {string} host + * @property {string} walletId + */ + +/** + * @param {import('contracts-js').ValidatorRegistryContext} registry + * @param {import('web3').default} web3 + * @param {import('contracts-js').LumerinContext} lumerin + * @returns {function(RegisterValidatorRequest): Promise} Function that takes a request object and registers a validator + */ +const registerValidator = (registry, web3, lumerin) => async (request) => { + const privateKey = request.privateKey; + const tempWallet = ethereumWallet.fromPrivateKey( + Buffer.from(remove0xPrefix(privateKey), 'hex') + ) + const pubKey = add65BytesPrefix(tempWallet.getPublicKey().toString('hex')); + const { yParity, x } = compressPublicKey(pubKey); + + const account = web3.eth.accounts.privateKeyToAccount(privateKey) + web3.eth.accounts.wallet.create(0).add(account) + + const increaseAllowanceEstimate = await lumerin.methods + .increaseAllowance(registry.options.address, request.stake) + .estimateGas({ + from: request.walletId, + }) + + await lumerin.methods + .increaseAllowance(registry.options.address, request.stake) + .send({ + from: request.walletId, + gas: increaseAllowanceEstimate, + }) + + try { + const estimatedGas = await registry.methods + .validatorRegister(request.stake, yParity, x, request.host) + .estimateGas({ from: request.walletId }); + + await registry.methods.validatorRegister( + request.stake, yParity, x, request.host) + .send({ from: request.walletId, gas: estimatedGas }); + } catch (err) { + console.log("validator register error", err) + throw err + } +} + +/** @param {Uint8Array|string} pubKey */ +const compressPublicKey = (pubKey) => { + const point = secp256k1.ProjectivePoint.fromHex(pubKey); + const compressed = point.toRawBytes(true); + + return { + yParity: compressed[0] === hexToBytes("03")[0], + x: "0x" + bytesToHex(compressed.slice(1)), + }; +} + +/** @param {boolean} yParity @param {`0x${string}`} x */ +const decompressPublicKey = (yParity, x) => { + const xBytes = hexToBytes(x.replace("0x", "")) + + const rec = new Uint8Array(33); + rec.set(hexToBytes(yParity ? "03" : "02")); + rec.set(xBytes, 1); + + const decompressed = secp256k1.ProjectivePoint.fromHex(bytesToHex(rec)); + + return "0x" + bytesToHex(decompressed.toRawBytes(false)); +} + +/** + * Returns solidity function (error, event) selector hash of the given signature + * @param {string} signature + * */ +const getHash = (signature) => { + return keccak256(signature).slice(0, 10) +} + +module.exports = { + getValidator, + getValidators, + getValidatorsMinimalStake, + getValidatorsRegisterStake, + registerValidator, + decompressPublicKey, + deregisterValidator +} diff --git a/src/plugins/validator-registry/index.js b/src/plugins/validator-registry/index.js new file mode 100644 index 0000000..a351af0 --- /dev/null +++ b/src/plugins/validator-registry/index.js @@ -0,0 +1,45 @@ +// @ts-check +const logger = require('../../logger'); +const api = require('./api') +const { ValidatorRegistry, Lumerin } = require('contracts-js') + +function createPlugin() { + + function start({ config, plugins }) { + const { + lmrTokenAddress, + validatorRegistryAddress + } = config + const { eth } = plugins + + const web3 = eth.web3 + + const lumerin = Lumerin(web3, lmrTokenAddress) + const registry = ValidatorRegistry(web3, validatorRegistryAddress) + + return { + api: { + getValidator: api.getValidator(registry), + getValidators: api.getValidators(registry, web3), + registerValidator: api.registerValidator(registry, web3, lumerin), + deregisterValidator: api.deregisterValidator(registry, web3), + getValidatorsMinimalStake: api.getValidatorsMinimalStake(registry), + getValidatorsRegisterStake: api.getValidatorsRegisterStake(registry) + }, + events: [ + ], + name: 'validator-registry', + } + } + + function stop() { + logger.debug('Plugin stopping') + } + + return { + start, + stop, + } +} + +module.exports = createPlugin