Skip to content

Commit

Permalink
Merge branch 'main' into fix-displayed-version
Browse files Browse the repository at this point in the history
  • Loading branch information
Shadowfiend authored Feb 28, 2022
2 parents a5c7155 + 7a3c222 commit bae6389
Show file tree
Hide file tree
Showing 31 changed files with 1,022 additions and 353 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ $ yarn start # start a continuous webpack build that will auto-update with chang
Once the build is running, you can install the extension in your browser of choice:

- [Firefox instructions](https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/)
- [Chrome, Brave, and Opera instructions](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest)
- [Chrome, Brave, Edge, and Opera instructions](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest)
- Note that these instructions are for Chrome, but substituting
`brave://extensions` or `opera://extensions` for `chrome://extensions`
`brave://extensions` or `edge://extensions` or `opera://extensions` for `chrome://extensions`
depending on browser should get you to the same buttons.

Extension bundles for each browser are in `dist/<browser>`.
Expand Down Expand Up @@ -291,11 +291,13 @@ src/ # extension source files
dist/ # output directory for builds
brave/ # browser-specific
firefox/ # build
chrome/ # directories
brave.zip # browser-specific
firefox.zip # production
chrome.zip # bundles
chrome/ # build
edge/ # directories
firefox/
brave.zip # browser-specific
chrome.zip # production
edge.zip # bundles
firefox.zip
build-utils/ # build-related helpers, used in webpack.config.js
*.js
Expand Down
115 changes: 115 additions & 0 deletions background/lib/asset-similarity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { AnyAsset, isSmartContractFungibleAsset } from "../assets"
import { normalizeEVMAddress } from "./utils"

/**
* Use heuristics to score two assets based on their metadata similarity. The
* higher the score, the more likely the asset metadata refers to the same
* asset.
*
* @param a - the first asset
* @param b - the second asset
* @return an integer score >= 0
*/
export function scoreAssetSimilarity(a: AnyAsset, b: AnyAsset): number {
let score = 0
if (a.symbol === b.symbol) {
score += 1
}
if (a.name === b.name) {
score += 1
}
if ("decimals" in a && "decimals" in b) {
if (a.decimals === b.decimals) {
score += 1
} else {
score -= 1
}
} else if ("decimals" in a || "decimals" in b) {
score -= 1
}
if ("homeNetwork" in a && "homeNetwork" in b) {
const sameNetwork =
a.homeNetwork.name === b.homeNetwork.name &&
a.homeNetwork.chainID === b.homeNetwork.chainID
if (sameNetwork) {
score += 1
} else {
score -= 1
}
} else if ("homeNetwork" in a || "homeNetwork" in b) {
score -= 1
}
return score
}

/**
* Returns a prioritized list of similarity keys, which are strings that can be
* used to rapidly correlate assets. All similarity keys should be further
* checked using {@link assetsSufficientlySimilar}, as a similarity key match
* is designed to narrow the field rather than guarantee asset sameness.
*/
export function prioritizedAssetSimilarityKeys(asset: AnyAsset): string[] {
let similarityKeys: string[] = []

if (isSmartContractFungibleAsset(asset)) {
const normalizedContractAddressAndNetwork = `${normalizeEVMAddress(
asset.contractAddress
)}-${asset.homeNetwork.chainID}`

similarityKeys = [...similarityKeys, normalizedContractAddressAndNetwork]
}

return [...similarityKeys, asset.symbol]
}

/**
* Score a set of assets by similarity to a search asset, returning the most
* similiar asset to the search asset as long as it is above a base similiarity
* score, or null.
*
* @see scoreAssetSimilarity The way asset similarity is computed.
*
* @param assetToFind The asset we're trying to find.
* @param assets The array of assets in which to search for `assetToFind`.
* @param minimumSimilarityScore The minimum similarity score to consider as a
* match.
*/
export function findClosestAssetIndex(
assetToFind: AnyAsset,
assets: AnyAsset[],
minimumSimilarityScore = 2
): number | undefined {
const [bestScore, index] = assets.reduce(
([runningScore, runningScoreIndex], asset, i) => {
const score = scoreAssetSimilarity(assetToFind, asset)
if (score > runningScore) {
return [score, i]
}
return [runningScore, runningScoreIndex]
},
[0, -1]
)

if (bestScore >= minimumSimilarityScore && index >= 0) {
return index
}

return undefined
}

/**
* Merges the information about two assets. Mostly focused on merging metadata.
*/
export function mergeAssets(asset1: AnyAsset, asset2: AnyAsset): AnyAsset {
return {
...asset1,
metadata: {
...asset1.metadata,
...asset2.metadata,
tokenLists:
asset1.metadata?.tokenLists?.concat(
asset2.metadata?.tokenLists ?? []
) ?? [],
},
}
}
4 changes: 2 additions & 2 deletions background/lib/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { AlchemyProvider, BaseProvider } from "@ethersproject/providers"
import { BigNumber, ethers, logger } from "ethers"
import { BigNumber, ethers } from "ethers"
import {
EventFragment,
Fragment,
FunctionFragment,
TransactionDescription,
} from "ethers/lib/utils"
import { getTokenBalances, getTokenMetadata } from "./alchemy"
import { getTokenBalances } from "./alchemy"
import { AccountBalance, AddressOnNetwork } from "../accounts"
import { SmartContractFungibleAsset } from "../assets"
import { EVMLog } from "../networks"
Expand Down
106 changes: 43 additions & 63 deletions background/lib/tokenList.ts → background/lib/token-lists.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { TokenList } from "@uniswap/token-lists"

import { normalizeEVMAddress } from "./utils"
import {
FungibleAsset,
isSmartContractFungibleAsset,
SmartContractFungibleAsset,
TokenListAndReference,
} from "../assets"
import { isValidUniswapTokenListResponse } from "./validate"
import { EVMNetwork } from "../networks"
import {
findClosestAssetIndex,
prioritizedAssetSimilarityKeys,
} from "./asset-similarity"

export async function fetchAndValidateTokenList(
url: string
Expand Down Expand Up @@ -66,89 +68,67 @@ function tokenListToFungibleAssetsForNetwork(

/**
* Merges the given asset lists into a single deduplicated array.
*
* Note that currently, two smart contract assets that are the same but don't
* share a contract address (e.g., a token A that points to a contract address
* and a token A that points to a proxy A's contract address) will not be
* considered the same for merging purposes.
*/
export function mergeAssets<T extends FungibleAsset>(
...assetLists: T[][]
): T[] {
function tokenReducer(
seenAssetsBy: {
contractAddressAndNetwork: {
[contractAddressAndNetwork: string]: SmartContractFungibleAsset
}
symbol: { [symbol: string]: T }
seenAssetsBySimilarityKey: {
[similarityKey: string]: T[]
},
asset: T
) {
const updatedAssetsBy = {
contractAddressAndNetwork: { ...seenAssetsBy.contractAddressAndNetwork },
symbol: { ...seenAssetsBy.symbol },
}
const updatedSeenAssetsBySimilarityKey = { ...seenAssetsBySimilarityKey }

if (isSmartContractFungibleAsset(asset)) {
const normalizedContractAddressAndNetwork =
`${normalizeEVMAddress(asset.contractAddress)}-${
asset.homeNetwork.chainID
}` ?? asset.homeNetwork.name
const existingAsset =
updatedAssetsBy.contractAddressAndNetwork[
normalizedContractAddressAndNetwork
]

if (typeof existingAsset !== "undefined") {
updatedAssetsBy.contractAddressAndNetwork[
normalizedContractAddressAndNetwork
] = {
...existingAsset,
metadata: {
...existingAsset.metadata,
...asset.metadata,
tokenLists:
existingAsset.metadata?.tokenLists?.concat(
asset.metadata?.tokenLists ?? []
) ?? [],
},
}
} else {
updatedAssetsBy.contractAddressAndNetwork[
normalizedContractAddressAndNetwork
] = asset
}
} else if (asset.symbol in updatedAssetsBy.symbol) {
const original = updatedAssetsBy.symbol[asset.symbol]
updatedAssetsBy.symbol[asset.symbol] = {
...original,
const similarityKeys = prioritizedAssetSimilarityKeys(asset)

// For now, only use the highest-priority similarity key with no fallback.
const referenceKey = similarityKeys[0]
// Initialize if needed.
updatedSeenAssetsBySimilarityKey[referenceKey] ??= []

// For each key, determine where a close asset match exists.
const matchingAssetIndex = findClosestAssetIndex(
asset,
updatedSeenAssetsBySimilarityKey[referenceKey]
)

if (typeof matchingAssetIndex !== "undefined") {
// Merge the matching asset with this new one.
const matchingAsset =
updatedSeenAssetsBySimilarityKey[referenceKey][matchingAssetIndex]

updatedSeenAssetsBySimilarityKey[referenceKey][matchingAssetIndex] = {
...matchingAsset,
metadata: {
...original.metadata,
...matchingAsset.metadata,
...asset.metadata,
tokenLists:
original.metadata?.tokenLists?.concat(
matchingAsset.metadata?.tokenLists?.concat(
asset.metadata?.tokenLists ?? []
) ?? [],
},
}
} else {
updatedAssetsBy.symbol[asset.symbol] = asset
updatedSeenAssetsBySimilarityKey[referenceKey].push(asset)
}

return updatedAssetsBy
return updatedSeenAssetsBySimilarityKey
}

const mergedAssetsBy = assetLists.flat().reduce(tokenReducer, {
contractAddressAndNetwork: {},
symbol: {},
})
const mergedAssets = Object.values(mergedAssetsBy.symbol).concat(
// Because the inputs to the function conform to T[], if T is not a subtype
// of SmartContractFungibleAsset, this will be an empty array. As such, we
// can safely do this cast.
Object.values(mergedAssetsBy.contractAddressAndNetwork) as unknown as T[]
)
const mergedAssetsBy = assetLists.flat().reduce(tokenReducer, {})
const mergedAssets = Object.values(mergedAssetsBy).flat()

return mergedAssets.sort((a, b) =>
(a.metadata?.tokenLists?.length || 0) >
(b.metadata?.tokenLists?.length || 0)
? 1
: -1
// Sort the merged assets by the number of token lists they appear in.
return mergedAssets.sort(
(a, b) =>
(a.metadata?.tokenLists?.length || 0) -
(b.metadata?.tokenLists?.length || 0)
)
}

Expand Down
Loading

0 comments on commit bae6389

Please sign in to comment.