UNPKG

@ixily/activ

Version:

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

788 lines (747 loc) 21.6 kB
import * as CryptoJS from 'crypto-js' import { CONTRACT_INTERFACES, LIT_ACTIONS, PricingModule, CONTRACT_TOOLS, getHashedMessage, LogModule as log, randomKeyWithLength, splitSignature, validateHashedSignedMessage, } from '../..' const state = { mockValidation: false as boolean, mockLitPricing: false as boolean, } const config = (_config: { mockValidation?: boolean mockLitPricing?: boolean }) => { if (_config.mockValidation !== undefined) { state.mockValidation = _config.mockValidation } if (_config.mockLitPricing !== undefined) { state.mockLitPricing = _config.mockLitPricing } } /* const checkValidNewIdeaFromChronological = async ( idea: ITradeIdea, chronological: IOpenSeaMetadata[], ): Promise<{ validation: string | IRuleValidResult chronological: IOpenSeaMetadata[] }> => { const chronologicalIdeas = chronological.map((each) => each.idea!) chronologicalIdeas.push(idea) // console.log('chronologicalIdeas') // console.log(chronologicalIdeas) return { validation: await rules(chronologicalIdeas, undefined, true), chronological, } } */ /* * 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 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 MAXIMUM_TIMESTAMP_DISTANCE = 400 // 200 ms const MAXIMUM_PRICE_DISTANCE = 0.04 // 4% 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 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 checkIfPriceSignedResponseIsValid = async ( pricingDetails: CONTRACT_INTERFACES.IValidPrice, ): Promise<{ valid: boolean; price: CONTRACT_INTERFACES.IValidPrice }> => { if (state.mockValidation) { return { valid: true, price: pricingDetails, } } const hashedMessage = getHashedMessage( CONTRACT_TOOLS.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 = LIT_ACTIONS.GET_PRICE_BINANCE_V2.keyEthAddress } else if (pricingDetails.price!.provider === 'IEX') { expectedAddress = LIT_ACTIONS.GET_PRICE_IEX_V2.keyEthAddress } else if (pricingDetails.price!.provider === 'IG Group') { expectedAddress = LIT_ACTIONS.GET_PRICE_IG_V2.keyEthAddress } else if (pricingDetails.price!.provider === 'Alpaca') { expectedAddress = LIT_ACTIONS.GET_PRICE_ALPACA_V2.keyEthAddress } else if (pricingDetails.price!.provider === 'Tradestation') { expectedAddress = LIT_ACTIONS.GET_PRICE_TRADESTATION_V2.keyEthAddress } else if (pricingDetails.price!.provider === 'CoinMarketCap') { expectedAddress = LIT_ACTIONS.GET_PRICE_COINMARKETCAP_V2.keyEthAddress } else if (pricingDetails.price!.provider === 'CryptoCompare') { expectedAddress = LIT_ACTIONS.GET_PRICE_CRYPTOCOMPARE_V2.keyEthAddress } else { throw new Error( 'TradeIdeaPricingValidationError: Invalid pricing provider', ) } const verification = await validateHashedSignedMessage(hashedSignedMessage) log.dev('verification:', verification) // we let off while not debugged if (verification !== expectedAddress) { return { valid: false, price: pricingDetails, } } else { return { valid: true, price: pricingDetails, } } } const verifyPriceDetails = async ( pricingDetails: CONTRACT_INTERFACES.IValidPrice, symbol: string, ): Promise<{ valid: boolean; price: CONTRACT_INTERFACES.IValidPrice }> => { if (state.mockValidation) { return { valid: true, price: { ...pricingDetails, priceParts: pricingDetails.response as unknown as CONTRACT_INTERFACES.IPriceOrErrorOrSkip[], price: JSON.parse(pricingDetails.response[0]), }, } } // First we restore the price parts from key parts let priceParts: CONTRACT_INTERFACES.IPriceOrErrorOrSkip[] = [] try { priceParts = restorePricePartsFromKeys( pricingDetails.response, pricingDetails.keys, ) } catch (e) { log.prod('Error Validating Prices: ' + symbol + ' :') log.prod(e) return { valid: false, price: pricingDetails, } } pricingDetails.priceParts = priceParts // Now we validate the got prices and reduce to one price let price: CONTRACT_INTERFACES.IPriceOrError try { price = getValidPriceFromPartsAndKeys( pricingDetails.priceParts, pricingDetails.keys, ) } catch (e) { log.prod('Error Validating Prices: ' + symbol + ' :') log.prod(e) return { valid: false, price: pricingDetails, } } log.dev('symbol:') log.dev(symbol) throwPricingError(price) pricingDetails.price = price as CONTRACT_INTERFACES.IPrice // console.log('price:') // console.log(price) // For last, we validate the original responseArray signature return checkIfPriceSignedResponseIsValid(pricingDetails) } const getVerifiedPriceDetails = async ( symbol: string, provider: CONTRACT_INTERFACES.IPricingProvider, auth?: CONTRACT_INTERFACES.ITradeIdeaPricingCredentials, customSettings?: { tracker?: string croupierUrl?: string }, ): Promise<CONTRACT_INTERFACES.IValidPrice> => { if (customSettings?.tracker === undefined) { customSettings = { ...customSettings, tracker: randomKeyWithLength(7), } log.dev( 'ACTIV: getVerifiedPriceDetails: The tracker was undefined, changing it to string:', customSettings?.tracker, ) } const maxAttempts = 3 let attempts = 0 // to avoid mutating the original auth object (IG especially) let authCopy = auth !== undefined ? JSON.stringify(auth) : 'undefined' while (attempts < maxAttempts) { auth = authCopy !== 'undefined' ? JSON.parse(authCopy) : undefined const pricingDetails = await PricingModule.getPriceDetails( { params: { symbol, }, provider, auth, }, state.mockLitPricing, { attempts, tracker: customSettings?.tracker, croupierUrl: customSettings?.croupierUrl, }, ) if (state.mockLitPricing) { return { ...pricingDetails, priceParts: [JSON.parse(pricingDetails.response[0])], price: JSON.parse(pricingDetails.response[0]), } } const verification = await verifyPriceDetails(pricingDetails, symbol) log.dev( 'IG_PRE_AUTHENTICATE (verification --> ¿is valid?)', verification.valid, ) log.dev( 'IG_PRE_AUTHENTICATE (verification --> price)', verification.price.price, ) if (verification.valid) { // console.log('verification') // console.log(verification) return { ...pricingDetails, priceParts: verification.price.priceParts, price: verification.price.price, } } else { log.prod('Invalid signatures') } attempts++ // await rest(200) } throw new Error( 'TradeIdeaPricingValidationError: Could not get valid price details', ) } const throwPricingError = ( price: CONTRACT_INTERFACES.IPrice | CONTRACT_INTERFACES.IPriceError, ) => { if ((price as CONTRACT_INTERFACES.IPriceError).fetchError === true) { const err = price as CONTRACT_INTERFACES.IPriceError if (err.knownError !== undefined) { throw new Error('Pricing Error: ' + err.knownError) } else { throw new Error('Unknown Pricing Error: ' + err.unknownError) } } } const validatePricingSignature = async ( idea: CONTRACT_INTERFACES.ITradeIdea, ) => { const ideaIdea = idea.idea as CONTRACT_INTERFACES.ITradeIdeaIdea const pricingDetails = ideaIdea.priceInfo! // console.log('pricingDetails:') // console.log(pricingDetails) if (state.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) { log.prod('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) { log.prod('Error Validating Prices:') log.prod(e) return { valid: false, price: pricingDetails, } } if ( CONTRACT_TOOLS.deterministicStringify(priceParts) !== CONTRACT_TOOLS.deterministicStringify(pricingDetails.priceParts) ) { log.prod('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) { log.prod('Error Validating Prices:') log.prod(e) return { valid: false, price: pricingDetails, } } throwPricingError(price) if ( CONTRACT_TOOLS.deterministicStringify(price) !== CONTRACT_TOOLS.deterministicStringify(pricingDetails.price) ) { log.prod('TradeIdeaPricingValidationError: price do not match') return { valid: false, price: pricingDetails, } } // For last, we validate the original responseArray signature return checkIfPriceSignedResponseIsValid(pricingDetails) } export const ProvableModule = { config, // checkValidNewIdeaFromChronological, getVerifiedPriceDetails, validatePricingSignature, }