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