Skip to content

Commit

Permalink
Still WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Shadowfiend committed Oct 16, 2021
1 parent e22657b commit 5cfc1f6
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 229 deletions.
21 changes: 16 additions & 5 deletions background/lib/alchemy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { utils } from "ethers"
import logger from "./logger"
import { AssetTransfer, HexString, SmartContractFungibleAsset } from "../types"
import { ETH, ETHEREUM } from "../constants"
import { jtdValidatorFor } from "./validation"

const ajv = new Ajv()

Expand Down Expand Up @@ -49,8 +50,9 @@ type AlchemyAssetTransferResponse = JTDDataType<
typeof alchemyGetAssetTransfersJTD
>

const isValidAlchemyAssetTransferResponse =
ajv.compile<AlchemyAssetTransferResponse>(alchemyGetAssetTransfersJTD)
const isValidAlchemyAssetTransferResponse = jtdValidatorFor(
alchemyGetAssetTransfersJTD
)

/**
* Use Alchemy's getAssetTransfers call to get historical transfers for an
Expand Down Expand Up @@ -144,7 +146,7 @@ export async function getAssetTransfers(
dataSource: "alchemy",
} as AssetTransfer
})
.filter((t) => t)
.filter((t): t is AssetTransfer => t !== null)
}

// JSON Type Definition for the Alchemy token balance API.
Expand Down Expand Up @@ -204,7 +206,16 @@ export async function getTokenBalances(

// TODO log balances with errors, consider returning an error type
return json.tokenBalances
.filter((b) => b.error === null && b.tokenBalance !== null)
.filter(
(
b
): b is typeof json["tokenBalances"][0] & {
tokenBalance: Exclude<
typeof json["tokenBalances"][0]["tokenBalance"],
null
>
} => b.error === null && b.tokenBalance !== null
)
.map((tokenBalance) => ({
contractAddress: tokenBalance.contractAddress,
amount:
Expand Down Expand Up @@ -262,8 +273,8 @@ export async function getTokenMetadata(
name: json.name,
symbol: json.symbol,
metadata: {
logoURL: json.logo,
tokenLists: [],
...(json.logo ? { logoURL: json.logo } : {}),
},
homeNetwork: ETHEREUM, // TODO make multi-network friendly
contractAddress,
Expand Down
52 changes: 23 additions & 29 deletions background/lib/prices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,7 @@ export async function getPrice(
return null
}

return json
? parseFloat(
json[coingeckoCoinId][currencySymbol] as string // FIXME Drop as when strict mode arrives and price schema type can include this.
)
: null
return json?.[coingeckoCoinId]?.[currencySymbol] || null
}

function multiplyByFloat(n: bigint, f: number, precision: number) {
Expand Down Expand Up @@ -67,34 +63,32 @@ export async function getPrices(
validate.errors
)

return null
return []
}

return assets.reduce((acc, asset) => {
const resolutionTime = Date.now()
return assets.flatMap((asset) => {
const simpleCoinPrices = json[asset.metadata.coinGeckoId]
return acc.concat(
vsCurrencies
.map((c) => {
const symbol = c.symbol.toLowerCase()
if (symbol in simpleCoinPrices) {
return {
pair: [c, asset],
amounts: [
multiplyByFloat(
BigInt(10) ** BigInt(c.decimals),
simpleCoinPrices[symbol] as number, // FIXME Drop as when strict mode arrives and price schema type can include this.
8
),
BigInt(1),
],
time: Date.now(),
} as PricePoint

return vsCurrencies
.map<PricePoint | undefined>((c) => {
const symbol = c.symbol.toLowerCase()
const coinPrice = simpleCoinPrices?.[symbol]

if (coinPrice) {
return {
pair: [c, asset],
amounts: [
multiplyByFloat(BigInt(10) ** BigInt(c.decimals), coinPrice, 8),
BigInt(1),
],
time: resolutionTime,
}
return undefined
})
.filter((p) => p)
)
}, [])
}
return undefined
})
.filter((p): p is PricePoint => p !== undefined)
})
}

/*
Expand Down
9 changes: 5 additions & 4 deletions background/lib/tokenList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ export function networkAssetsFromLists(
metadata: {
...original.metadata,
...asset.metadata,
tokenLists: original.metadata.tokenLists.concat(
asset.metadata.tokenLists
),
tokenLists:
original.metadata?.tokenLists?.concat(
asset.metadata?.tokenLists ?? []
) ?? [],
},
}
} else {
Expand All @@ -99,7 +100,7 @@ export function networkAssetsFromLists(

const merged = fungibleAssets.reduce(tokenReducer, {})
return Object.entries(merged)
.map(([k, v]) => v)
.map(([, v]) => v)
.slice()
.sort((a, b) =>
(a.metadata?.tokenLists?.length || 0) >
Expand Down
163 changes: 125 additions & 38 deletions background/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { JSONSchemaType } from "ajv"
import Ajv, { JTDDataType } from "ajv/dist/jtd"
import { AnyValidateFunction } from "ajv/dist/types"
import logger from "./logger"
import { ValidateFunction } from "ajv/dist/types"

/**
* TODO create the proper organisation for the validation when using it to validate anything else
Expand All @@ -9,6 +9,15 @@ import logger from "./logger"
*/
const ajv = new Ajv()

export type CoingeckoPriceData = {
[coinId: string]:
| {
last_updated_at: number
[currencyId: string]: number | undefined
}
| undefined
}

/**
* https://github.com/ajv-validator/ajv/blob/master/spec/types/jtd-schema.spec.ts - jtd unit tests
* https://ajv.js.org/json-type-definition.html - jtd spec ajv
Expand All @@ -17,45 +26,123 @@ const ajv = new Ajv()
* https://ajv.js.org/guide/typescript.html - using with ts
*
*/
const coingeckoPriceSchema = {
values: {
// Ajv's typing incorrectly requires nullable: true for last_updated_at because
// the remaining keys in the coin entry are optional. This in turn interferes
// with the fact that last_updated_at is listed in `required`. The two `as`
// type casts below trick the type system into allowing the schema correctly.
// Note that the schema will validate as required, and the casts allow it to
// match the corret TypeScript types.
//
// This all stems from Ajv also incorrectly requiring an optional property (`|
// undefined`) to be nullable (`| null`). See
// https://github.com/ajv-validator/ajv/issues/1664, which should be fixed in
// Ajv v9 via
// https://github.com/ajv-validator/ajv/commit/b4b806fd03a9906e9126ad86cef233fa405c9a3e
const coingeckoPriceSchema: JSONSchemaType<CoingeckoPriceData> = {
type: "object",
required: [],
additionalProperties: {
type: "object",
properties: {
last_updated_at: { type: "uint32" },
last_updated_at: { type: "number" } as {
type: "number"
nullable: true
},
},
additionalProperties: true,
required: ["last_updated_at"] as never[],
additionalProperties: { type: "number", nullable: true },
nullable: true,
},
} as const

type CoinGeckoPriceDataJtd = JTDDataType<typeof coingeckoPriceSchema>

export function getSimplePriceValidator(): AnyValidateFunction<CoinGeckoPriceDataJtd> {
const cacheKey = "coingecko_simple_price"
let validate = ajv.getSchema<CoinGeckoPriceDataJtd>(cacheKey)

/**
* Schema compile is a costly operation so we want to do it as lazyly as possible.
* Parse it only when it's requested and do this only once and compile it only when used.
*
* The same could be achieved with using a $id in the cache but that keyword is
* not known in jtd strict mode. Adding it as an exstra keyword felt hacky and did not really work.
* https://ajv.js.org/guide/managing-schemas.html#pre-adding-all-schemas-vs-adding-on-demand
*/
if (validate) return validate

try {
/**
* addSchema does not compile the schema, but it's not necessary - be lazy
* https://ajv.js.org/api.html#ajv-addschema-schema-object-object-key-string-ajv
* > Although addSchema does not compile schemas, explicit compilation is not required
* > the schema will be compiled when it is used first time.
*/
ajv.addSchema(coingeckoPriceSchema, cacheKey)
validate = ajv.getSchema<CoinGeckoPriceDataJtd>(cacheKey)
} catch (e) {
logger.error(e)
}

return validate
}

type EnvlessValidateFunction<T> = ((json: unknown) => json is T) &
Omit<ValidateFunction<T> | ValidateFunction<JTDDataType<T>>, "schemaEnv">

/**
* Returns a lazily-compiled JTD validator from a central Ajv instance.
*/
export function jtdValidatorFor<SchemaType>(
jtdDefinition: SchemaType
): EnvlessValidateFunction<JTDDataType<SchemaType>> {
let compiled: ValidateFunction<JTDDataType<SchemaType>> | null = null

const wrapper: EnvlessValidateFunction<JTDDataType<SchemaType>> =
Object.assign(
(json: unknown): json is JTDDataType<SchemaType> => {
try {
compiled =
compiled || ajv.compile<JTDDataType<SchemaType>>(jtdDefinition)

const result = compiled(json)
// Copy errors and such, which Ajv carries on the validator function
// object itself.
Object.assign(wrapper, result)

return result
} catch (error) {
// If there's a compilation error, communicate it in a way that
// aligns with Ajv's typical way of communicating validation errors,
// and report the JSON as invalid (since we can't know for sure).
wrapper.errors = [
{
keyword: "COMPILATION FAILURE",
params: { error },
instancePath: "",
schemaPath: "",
},
]

return false
}
},
{ schema: jtdDefinition }
)

return wrapper
}

/**
* Returns a lazily-compiled JSON Schema validator from a central Ajv instance.
*/
export function jsonSchemaValidatorFor<T>(
jsonSchemaDefinition: JSONSchemaType<T>
): EnvlessValidateFunction<T> {
let compiled: ValidateFunction<T> | null = null

const wrapper: EnvlessValidateFunction<T> = Object.assign(
(json: unknown): json is T => {
try {
compiled = compiled || ajv.compile<T>(jsonSchemaDefinition)
const result = compiled(json)
// Copy errors and such, which Ajv carries on the validator function
// object itself.
Object.assign(wrapper, result)

return result
} catch (error) {
// If there's a compilation error, communicate it in a way that
// aligns with Ajv's typical way of communicating validation errors,
// and report the JSON as invalid (since we can't know for sure).
wrapper.errors = [
{
keyword: "COMPILATION FAILURE",
params: { error },
instancePath: "",
schemaPath: "",
},
]

return false
}
},
{ schema: jsonSchemaDefinition }
)

return wrapper
}

export function getSimplePriceValidator(): EnvlessValidateFunction<CoingeckoPriceData> {
return jsonSchemaValidatorFor<CoingeckoPriceData>(coingeckoPriceSchema)
}

// TODO implement me - I need at least a contract address to test this
Expand Down
Loading

0 comments on commit 5cfc1f6

Please sign in to comment.