UNPKG

@ixily/activ

Version:

Alpha Capture Trade Idea Verification. Blockchain ownership proven trade ideas and strategies.

656 lines (627 loc) 18.6 kB
import * as CryptoJS from 'crypto-js' import { ethers } from 'ethers' import { deterministicStringify } from '../' import { CONTRACT_CONSTANTS, CONTRACT_INTERFACES } from '../../' const MAXIMUM_TIMESTAMP_DISTANCE = 400 // 200 ms const MAXIMUM_PRICE_DISTANCE = 0.04 // 4% const splitSignature = ( signature: string, ): { r: string s: string v: number } => { const r = signature.slice(0, 66) const s = '0x' + signature.slice(66, 130) const v = parseInt(signature.slice(130, 132), 16) return { r, s, v } } /* * Uses crypto-js to decrypt the price part * response is the encrypted price part as string * key is the key part as string * cryptography used function PBKDF2 with SHA-256 * response is concatenation of, in order: salt, iv and ciphertext. */ const decryptPricePart = ( response: string, password: string, ): CONTRACT_INTERFACES.IPriceOrErrorOrSkip => { const encrypted = CryptoJS.enc.Base64.parse(response) const salt_len = 16 const iv_len = 16 const salt = CryptoJS.lib.WordArray.create( encrypted.words.slice(0, salt_len / 4), ) const iv = CryptoJS.lib.WordArray.create( encrypted.words.slice(0 + salt_len / 4, (salt_len + iv_len) / 4), ) const key = CryptoJS.PBKDF2(password, salt, { keySize: 256 / 32, iterations: 10000, hasher: CryptoJS.algo.SHA256, }) const decrypted = CryptoJS.AES.decrypt( CryptoJS.lib.WordArray.create( encrypted.words.slice((salt_len + iv_len) / 4), ).toString(CryptoJS.enc.Base64), key, { iv: iv }, ) return JSON.parse( decrypted.toString(CryptoJS.enc.Utf8), ) as unknown as CONTRACT_INTERFACES.IPriceOrErrorOrSkip } const testForInvalidNoFetchNodes = ( priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[], keys: [string, string][], ) => { for (const pricePartIndex in priceParts) { const pricePart = priceParts[pricePartIndex] // validate the skipped parts if ((pricePart as CONTRACT_INTERFACES.ISkipPrice).noFetch === true) { const skipped = pricePart as CONTRACT_INTERFACES.ISkipPrice if (skipped.ckWd === undefined) { throw new Error( 'TradeIdeaPricingValidationError: noFetch ckWd undefined', ) } else { if (skipped.ckWd !== keys[pricePartIndex][0]) { throw new Error( 'TradeIdeaPricingValidationError: noFetch ckWd does not match', ) } } } } } const testForDealtErrors = ( priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[], ): undefined | CONTRACT_INTERFACES.IPriceError => { let listOfKnownErrors: CONTRACT_INTERFACES.IKnownFetchPriceError[] = [] let atLeastOneUnknownError: string | undefined = undefined for (const pricePartIndex in priceParts) { const pricePart = priceParts[pricePartIndex] // validate the skipped parts if ( (pricePart as CONTRACT_INTERFACES.IPriceError).fetchError === true ) { const err = pricePart as CONTRACT_INTERFACES.IPriceError if (err.unknownError !== undefined) { atLeastOneUnknownError = err.unknownError } else { if (err.knownError === undefined) { throw new Error( 'TradeIdeaPricingValidationError: fetchError knownError undefined when not expected', ) } if ( listOfKnownErrors.find((one) => one === err.knownError!) === undefined ) { listOfKnownErrors.push(err.knownError!) } } } } if (atLeastOneUnknownError !== undefined) { const pricingError: CONTRACT_INTERFACES.IPriceError = { fetchError: true, unknownError: atLeastOneUnknownError, } return pricingError } else { if (listOfKnownErrors.length === 0) { return undefined } else if (listOfKnownErrors.length === 1) { const pricingError: CONTRACT_INTERFACES.IPriceError = { fetchError: true, knownError: listOfKnownErrors[0], } return pricingError } else { const pricingError: CONTRACT_INTERFACES.IPriceError = { fetchError: true, unknownError: listOfKnownErrors.join(', '), } return pricingError } } } const restorePricePartsFromKeys = ( responses: any[], keys: [string, string][], ): CONTRACT_INTERFACES.IPriceOrErrorOrSkip[] => { const priceParts: any[] = [] for (let i = 0; i < responses.length; i++) { const response = responses[i] const key = keys[i] const pricePart = decryptPricePart(response, key[1]) // log.dev('pricePart') // log.dev(pricePart) priceParts.push(pricePart) } return priceParts } const getValidPriceFromPartsAndKeys = ( priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[], keys: [string, string][], ): CONTRACT_INTERFACES.IPrice | CONTRACT_INTERFACES.IPriceError => { // console.log('priceParts') // console.log(priceParts) // console.log('keys') // console.log(keys) testForInvalidNoFetchNodes(priceParts, keys) const pricingError = testForDealtErrors(priceParts) // console.log('pricingError') // console.log(pricingError) if (pricingError !== undefined) { return pricingError } let provider: CONTRACT_INTERFACES.IPricingProvider | undefined // required let symbol: string | undefined // required let company: string | undefined const globalPrices: number[] = [] // required let globalPrice: number = 0 // required let asks: number[] | undefined = undefined let ask: number | undefined = undefined let bids: number[] | undefined = undefined let bid: number | undefined = undefined const timestamps: number[] = [] // required let timestamp: number = 0 // required let fromProxy: string | undefined = undefined for (const pricePartIndex in priceParts) { const pricePart = priceParts[pricePartIndex] if (!((pricePart as CONTRACT_INTERFACES.ISkipPrice).noFetch === true)) { const _price = pricePart as CONTRACT_INTERFACES.IPrice // validate provider if (_price.provider === undefined) { throw new Error( 'TradeIdeaPricingValidationError: provider undefined', ) } if (typeof _price.provider !== 'string') { throw new Error( 'TradeIdeaPricingValidationError: provider not string', ) } if (provider === undefined) { provider = _price.provider } else { if (provider !== _price.provider) { throw new Error( 'TradeIdeaPricingValidationError: provider does not match', ) } } // validate symbol if (_price.symbol === undefined) { throw new Error( 'TradeIdeaPricingValidationError: symbol undefined', ) } if (typeof _price.symbol !== 'string') { throw new Error( 'TradeIdeaPricingValidationError: symbol not string', ) } if (symbol === undefined) { symbol = _price.symbol } else { if (symbol !== _price.symbol) { throw new Error( 'TradeIdeaPricingValidationError: symbol does not match', ) } } // validate company if (_price.company !== undefined) { if (typeof _price.company !== 'string') { throw new Error( 'TradeIdeaPricingValidationError: company not string', ) } if (company === undefined) { company = _price.company } else { if (company !== _price.company) { throw new Error( 'TradeIdeaPricingValidationError: company does not match', ) } } } else { if (company !== undefined) { throw new Error( 'TradeIdeaPricingValidationError: company does not match', ) } } // validate globalPrice if (_price.globalPrice === undefined) { throw new Error( 'TradeIdeaPricingValidationError: globalPrice undefined', ) } if (typeof _price.globalPrice !== 'number') { throw new Error( 'TradeIdeaPricingValidationError: globalPrice not number', ) } globalPrices.push(_price.globalPrice) // validate ask if (_price.ask !== undefined) { if (typeof _price.ask !== 'number') { throw new Error( 'TradeIdeaPricingValidationError: ask not number', ) } if (asks === undefined) { asks = [] } asks.push(_price.ask) } // validate bid if (_price.bid !== undefined) { if (typeof _price.bid !== 'number') { throw new Error( 'TradeIdeaPricingValidationError: bid not number', ) } if (bids === undefined) { bids = [] } bids.push(_price.bid) } // validate timestamp if (_price.timestamp === undefined) { throw new Error( 'TradeIdeaPricingValidationError: timestamp undefined', ) } if (typeof _price.timestamp !== 'number') { throw new Error( 'TradeIdeaPricingValidationError: timestamp not number', ) } timestamps.push(_price.timestamp) // validate origin if (_price.fromProxy !== undefined) { if (fromProxy === undefined) { fromProxy = _price.fromProxy } else { if (fromProxy !== _price.fromProxy) { throw new Error( 'TradeIdeaPricingValidationError: fromProxy does not match', ) } } } } } // calculate globalPrice // console.log('globalPrices') // console.log(globalPrices) let maxDecimalsGlobalPrice = 0 for (const _globalPrice of globalPrices) { const preDecimals = _globalPrice.toString().split('.') const decimals = preDecimals.length > 1 ? preDecimals[1].length : 0 if (decimals > maxDecimalsGlobalPrice) { maxDecimalsGlobalPrice = decimals } globalPrice += _globalPrice } globalPrice = globalPrice / globalPrices.length // restore globalPrice to its maximum decimals globalPrice = parseFloat(globalPrice.toFixed(maxDecimalsGlobalPrice)) // console.log('globalPrice') // console.log(globalPrice) // validate that globalPrices are not too far apart in a distance of constant // MAXIMUM_PRICE_DISTANCE, in percentage difference from average for (const _globalPrice of globalPrices) { const difference = Math.abs(_globalPrice - globalPrice) const percentageDifference = difference / globalPrice if (percentageDifference > MAXIMUM_PRICE_DISTANCE) { throw new Error( 'TradeIdeaPricingValidationError: globalPrice too far apart', ) } } // calculate average timestamp for (const _timestamp of timestamps) { timestamp += _timestamp } timestamp = Math.floor(timestamp / timestamps.length) // validate that timestamps are not too far apart in a distance of constant // MAXIMUM_TIMESTAMP_DISTANCE, in milliseconds for (const _timestamp of timestamps) { if (Math.abs(_timestamp - timestamp) > MAXIMUM_TIMESTAMP_DISTANCE) { throw new Error( 'TradeIdeaPricingValidationError: timestamp too far apart', ) } } // if applicable, calculate average ask and bid if (asks !== undefined) { ask = 0 let maxDecimalsAsk = 0 for (const _ask of asks) { const preDecimals = _ask.toString().split('.') const decimals = preDecimals.length > 1 ? preDecimals[1].length : 0 if (decimals > maxDecimalsAsk) { maxDecimalsAsk = decimals } ask += _ask } ask = ask / asks.length ask = parseFloat(ask.toFixed(maxDecimalsAsk)) // validate that ask prices are not too far apart in a distance of constant // MAXIMUM_PRICE_DISTANCE, in percentage difference from average for (const _ask of asks) { const difference = Math.abs(_ask - ask) const percentageDifference = difference / ask if (percentageDifference > MAXIMUM_PRICE_DISTANCE) { throw new Error( 'TradeIdeaPricingValidationError: ask too far apart', ) } } } if (bids !== undefined) { bid = 0 let maxDecimalsBid = 0 for (const _bid of bids) { const preDecimals = _bid.toString().split('.') const decimals = preDecimals.length > 1 ? preDecimals[1].length : 0 if (decimals > maxDecimalsBid) { maxDecimalsBid = decimals } bid += _bid } bid = bid / bids.length bid = parseFloat(bid.toFixed(maxDecimalsBid)) // validate that bid prices are not too far apart in a distance of constant // MAXIMUM_PRICE_DISTANCE, in percentage difference from average for (const _bid of bids) { const difference = Math.abs(_bid - bid) const percentageDifference = difference / bid if (percentageDifference > MAXIMUM_PRICE_DISTANCE) { throw new Error( 'TradeIdeaPricingValidationError: bid too far apart', ) } } } if (provider === undefined) { throw new Error('TradeIdeaPricingValidationError: provider undefined') } if (symbol === undefined) { throw new Error('TradeIdeaPricingValidationError: symbol undefined') } if (timestamp === 0) { throw new Error('TradeIdeaPricingValidationError: timestamp undefined') } if (globalPrice === 0) { throw new Error( 'TradeIdeaPricingValidationError: globalPrice undefined', ) } return { provider: provider!, symbol: symbol!, company, globalPrice, ask, bid, fromProxy, timestamp, } } const hashString = (valueStr: string) => { return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(valueStr)) } const getHashedMessage = ( originalMessage: string, ): CONTRACT_INTERFACES.IHashedMessage => { const hashedMessage = hashString(originalMessage) return { originalMessage, hashedMessage, } } const validateHashedSignedMessage = async ( hashedSignedMessage: CONTRACT_INTERFACES.IHashedSignedMessage, expectedAddress?: string, ): Promise<string> => { const probeHashed = getHashedMessage(hashedSignedMessage.originalMessage) if (probeHashed.originalMessage !== hashedSignedMessage.originalMessage) { throw new Error('The hashed message does not match original message') } const signerAddr = await ethers.utils.verifyMessage( hashedSignedMessage.hashedMessage, hashedSignedMessage.signature, ) if (expectedAddress !== undefined) { if (signerAddr !== expectedAddress) { throw new Error( `Signature address mismatch. Expected ${expectedAddress}, got ${signerAddr}`, ) } } return signerAddr } const checkIfPriceSignedResponseIsValid = async ( pricingDetails: CONTRACT_INTERFACES.IValidPrice, cachedStrategyState: CONTRACT_INTERFACES.IStrategyState, mockValidation: boolean = false, ): Promise<{ valid: boolean; price: CONTRACT_INTERFACES.IValidPrice }> => { if (mockValidation) { return { valid: true, price: pricingDetails, } } const hashedMessage = getHashedMessage( deterministicStringify(pricingDetails.response)!, ) // log.dev('pricingDetails.signature.signature:') // log.dev(pricingDetails.signature.signature) const signature = pricingDetails.signature.signature const splitedSignature = splitSignature(signature) const hashedSignedMessage: CONTRACT_INTERFACES.IHashedSignedMessage = { ...hashedMessage, signature, ...splitedSignature, } if (pricingDetails.price === undefined) { return { valid: false, price: pricingDetails, } } let expectedAddress: undefined | string = undefined if (pricingDetails.price!.provider === 'Binance') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.BINANCE_V2 } else if (pricingDetails.price!.provider === 'IEX') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.IEX_V2 } else if (pricingDetails.price!.provider === 'IG Group') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.IG_V2 } else if (pricingDetails.price!.provider === 'Alpaca') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.ALPACA_V2 } else if (pricingDetails.price!.provider === 'Tradestation') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.TRADESTATION_V2 } else if (pricingDetails.price!.provider === 'CoinMarketCap') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.COINMARKETCAP_V2 } else if (pricingDetails.price!.provider === 'CryptoCompare') { expectedAddress = CONTRACT_CONSTANTS.LIT_ACTIONS_ETH_ADDRESS.CRYPTOCOMPARE_V2 } else { throw new Error( 'TradeIdeaPricingValidationError: Invalid pricing provider', ) } const verification = await validateHashedSignedMessage(hashedSignedMessage) // console.log('verification:', verification) // we let off while not debugged if (verification !== expectedAddress) { return { valid: false, price: pricingDetails, } } else { return { valid: true, price: pricingDetails, } } } export const validatePricingSignature = async ( ideaIdea: CONTRACT_INTERFACES.ITradeIdeaIdea, cachedStrategyState: CONTRACT_INTERFACES.IStrategyState, mockValidation: boolean = false, ) => { const pricingDetails = ideaIdea.priceInfo! // console.log('pricingDetails:') // console.log(pricingDetails) if (mockValidation) { let priceIn: CONTRACT_INTERFACES.IPrice try { priceIn = JSON.parse(pricingDetails.response[0]) } catch (e) { priceIn = { globalPrice: 1, provider: 'Binance', symbol: 'BTCUSDT', timestamp: 1, } } return { valid: true, price: { ...pricingDetails, priceParts: pricingDetails.response as unknown as CONTRACT_INTERFACES.IPriceOrErrorOrSkip[], price: priceIn, }, } } if (ideaIdea.priceInfo === undefined) { // console.log('TradeIdeaPricingValidationError: priceInfo undefined') return { valid: false, price: pricingDetails, } } // First we restore the price parts from key parts let priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[] = [] try { priceParts = restorePricePartsFromKeys( pricingDetails.response, pricingDetails.keys, ) } catch (e) { // console.log('Error Validating Prices:') // console.log(e) return { valid: false, price: pricingDetails, } } if ( deterministicStringify(priceParts) !== deterministicStringify(pricingDetails.priceParts) ) { // console.log('TradeIdeaPricingValidationError: priceParts do not match') return { valid: false, price: pricingDetails, } } // Now we validate the got prices and reduce to one price let price: CONTRACT_INTERFACES.IPrice | CONTRACT_INTERFACES.IPriceError try { price = getValidPriceFromPartsAndKeys( pricingDetails.priceParts!, pricingDetails.keys, ) } catch (e) { // console.log('Error Validating Prices:') // console.log(e) return { valid: false, price: pricingDetails, } } if ((price as CONTRACT_INTERFACES.IPriceError).fetchError === true) { return { valid: false, price: pricingDetails, } } // console.log('price:') // console.log(price) // console.log('pricingDetails.price:') // console.log(pricingDetails.price) if ( deterministicStringify(price) !== deterministicStringify(pricingDetails.price) ) { // console.log('TradeIdeaPricingValidationError: price do not match') return { valid: false, price: pricingDetails, } } // console.log('price') // console.log(price) // console.log('pricingDetails') // console.log(pricingDetails.price) // For last, we validate the original responseArray signature return checkIfPriceSignedResponseIsValid( pricingDetails, cachedStrategyState, mockValidation, ) }