@ixily/activ
Version:
Alpha Capture Trade Idea Verification. Blockchain ownership proven trade ideas and strategies.
656 lines (627 loc) • 18.6 kB
text/typescript
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,
)
}