From 9797faa478891f9b9ebbab2cbf90c879d24339c6 Mon Sep 17 00:00:00 2001 From: Abdou <118254027+a6-dou@users.noreply.github.com> Date: Thu, 23 Mar 2023 15:06:02 +0400 Subject: [PATCH] fear: add blur.io visualization support --- src/types/blur.ts | 2 +- src/visualizer/blur-io/const.ts | 31 +++++++ src/visualizer/blur-io/index.ts | 93 +++++++++++++++++++ src/visualizer/index.ts | 7 ++ test/visualizer/blur-io/data.ts | 66 +++++++++++++ test/visualizer/blur-io/index.test.ts | 128 ++++++++++++++++++++++++++ 6 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 src/visualizer/blur-io/index.ts create mode 100644 test/visualizer/blur-io/data.ts create mode 100644 test/visualizer/blur-io/index.test.ts diff --git a/src/types/blur.ts b/src/types/blur.ts index bae0fe8..82e4d9c 100644 --- a/src/types/blur.ts +++ b/src/types/blur.ts @@ -10,7 +10,7 @@ export type BlurIoFee = { export type BlurIoOrder = { trader: string; - side: BlurIoSide; + side: BlurIoSide | string | number; matchingPolicy: string; collection: string; tokenId: string; diff --git a/src/visualizer/blur-io/const.ts b/src/visualizer/blur-io/const.ts index dadb1e7..416c3b8 100644 --- a/src/visualizer/blur-io/const.ts +++ b/src/visualizer/blur-io/const.ts @@ -1,2 +1,33 @@ +/// @dev fees denominator +export const BLUR_IO_INVERSE_BASIS_POINT = 10_000; + +/// @dev native ETH bidding pool address export const BLUR_IO_POOL_ADDRESS = "0x0000000000a39bb272e79075ade125fd351887ac".toLocaleLowerCase(); + +// blur.io policies +const BLUR_IO_STANDARD_POLICY_ERC721 = + "0x00000000006411739da1c40b106f8511de5d1fac".toLocaleLowerCase(); +const BLUR_IO_STANDARD_POLICY_ERC721_WITH_ORACLE = + "0x0000000000dab4a563819e8fd93dba3b25bc3495".toLocaleLowerCase(); +export const BLUR_IO_COLLECTION_BID_POLICY = + "0x0000000000b92d5d043faf7cecf7e2ee6aaed232".toLocaleLowerCase(); +/** + * @dev make sure that the address key is lower case + */ +const validPolicies = { + [BLUR_IO_STANDARD_POLICY_ERC721]: true, + [BLUR_IO_STANDARD_POLICY_ERC721_WITH_ORACLE]: true, + [BLUR_IO_COLLECTION_BID_POLICY]: true, +}; + +/** + * @dev blur.io order matching policies + * - MUST make sure that addresses are lower case to avoid False Positive caused by checksum + * @see https://www.notion.so/trustwallet/Blur-8afbcb6a262d488181a8c9ea417e7ce6?pvs=4#0ce31761c4d940d2bfeecfb268bb702d + * @param {HexaString} address the patching policy address, this param will get lower cased internally + * @returns {boolean} true if valid, false if not + */ +export const isValidBlurIoPolicy = (address: string): boolean => { + return validPolicies[address.toLocaleLowerCase()]; +}; diff --git a/src/visualizer/blur-io/index.ts b/src/visualizer/blur-io/index.ts new file mode 100644 index 0000000..ceae8d6 --- /dev/null +++ b/src/visualizer/blur-io/index.ts @@ -0,0 +1,93 @@ +import { PROTOCOL_ID } from ".."; +import { ASSET_TYPE, AssetInOut, Domain, Protocol, Result } from "../../types"; +import { BlurIoOrder, BlurIoSide } from "../../types/blur"; +import { ZERO_ADDRESS, getPaymentAssetType, isSameAddress } from "../../utils"; +import { + BLUR_IO_COLLECTION_BID_POLICY, + BLUR_IO_INVERSE_BASIS_POINT, + isValidBlurIoPolicy, +} from "./const"; + +const { ERC721, NATIVE } = ASSET_TYPE; +export const isCorrectDomain = (domain: Domain) => { + return ( + supportedChains.includes(Number(domain.chainId)) && + addressesBook.includes(domain.verifyingContract.toLocaleLowerCase()) + ); +}; + +export const visualize = (message: BlurIoOrder, domain: Domain): Result => { + if (!isCorrectDomain(domain)) throw new Error("wrong blur.io domain"); + if (!isValidBlurIoPolicy(message.matchingPolicy)) + throw new Error("unrecognized blur.io matching policy"); + + // Fees and Price calculation + const price = BigInt(message.price); + const feesDivider = BigInt(BLUR_IO_INVERSE_BASIS_POINT); + let accumulatedFees = BigInt(0); + message.fees.forEach((fee) => { + const rate = BigInt(fee.rate); + accumulatedFees += (price * rate) / feesDivider; + }); + const netPrice = price - accumulatedFees; + + /** + * @dev only ERC721 is supported by blur.io (at least for now). + * - MUST handle nftAsset.type if an ERC1155 policy is added + */ + const nftAsset: AssetInOut = { + address: message.collection, + type: ERC721, + id: message.tokenId, + amounts: [message.amount], + }; + + const paymentAssetType = getPaymentAssetType(message.paymentToken); + const paymentAsset: AssetInOut = { + address: paymentAssetType === NATIVE ? ZERO_ADDRESS : message.paymentToken, + type: paymentAssetType, + amounts: [], + }; + + // if it's a collection Bid + if (isSameAddress(message.matchingPolicy, BLUR_IO_COLLECTION_BID_POLICY)) + delete nftAsset.id; + + const assetIn: AssetInOut[] = []; + const assetOut: AssetInOut[] = []; + const tradeSide = Number(message.side); + if (tradeSide === BlurIoSide.Sell) { + paymentAsset.amounts.push(netPrice.toString()); + + assetOut.push(nftAsset); + assetIn.push(paymentAsset); + } else if (tradeSide === BlurIoSide.Buy) { + paymentAsset.amounts.push(price.toString()); + + assetOut.push(paymentAsset); + assetIn.push(nftAsset); + } else { + throw new Error("unrecognized blur.io order side"); + } + + return { + protocol: PROTOCOL_ID.BLUR_IO_MARKETPLACE, + assetIn, + assetOut, + liveness: { + from: Number(message.listingTime) * 1000, + to: Number(message.expirationTime) * 1000, + }, + }; +}; + +const supportedChains = [1]; +const addressesBook = [ + "0x000000000000ad05ccc4f10045630fb830b95127", // Mainnet +].map((e) => e.toLocaleLowerCase()); + +const blurIo: Protocol = { + isCorrectDomain, + visualize, +}; +export default blurIo; diff --git a/src/visualizer/index.ts b/src/visualizer/index.ts index 6022a5d..fc49624 100644 --- a/src/visualizer/index.ts +++ b/src/visualizer/index.ts @@ -1,6 +1,8 @@ import { Domain, Result } from "../types"; +import { BlurIoOrder } from "../types/blur"; import { LooksrareMakerOrderWithEncodedParams } from "../types/looksrare"; import { SeaPortPayload } from "../types/seaport"; +import blurIo from "./blur-io"; import looksrare from "./looksrare"; import seaport from "./seaport"; @@ -13,6 +15,8 @@ export enum PROTOCOL_ID { export const getProtocolId = (domain: Domain): PROTOCOL_ID | undefined => { if (seaport.isCorrectDomain(domain)) return PROTOCOL_ID.OPENSEA_SEAPORT; if (looksrare.isCorrectDomain(domain)) return PROTOCOL_ID.LOOKSRARE_EXCHANGE; + if (blurIo.isCorrectDomain(domain)) return PROTOCOL_ID.BLUR_IO_MARKETPLACE; + return; }; @@ -32,6 +36,9 @@ export default async function visualize(message: T, domain: Domain): Promise< case PROTOCOL_ID.LOOKSRARE_EXCHANGE: return looksrare.visualize(message as LooksrareMakerOrderWithEncodedParams, domain); + case PROTOCOL_ID.BLUR_IO_MARKETPLACE: + return blurIo.visualize(message as BlurIoOrder, domain); + default: throw new Error("Unrecognized/Unsupported Protocol Domain"); } diff --git a/test/visualizer/blur-io/data.ts b/test/visualizer/blur-io/data.ts new file mode 100644 index 0000000..9f4cf50 --- /dev/null +++ b/test/visualizer/blur-io/data.ts @@ -0,0 +1,66 @@ +const blurIoListing = { + trader: "0x900175B45Dcc84C23Cf597d5C3E766108CeA5bB0", + side: 1, + matchingPolicy: "0x0000000000daB4A563819e8fd93dbA3b25BC3495", + collection: "0x90c70Dc9f3FDa4a1D78a2B7D90CA087088355717", + tokenId: "12743", + amount: "1", + paymentToken: "0x0000000000000000000000000000000000000000", + price: "800000000000000000", + listingTime: "1677736151", + expirationTime: "1678340951", + fees: [ + { + rate: "500", + recipient: "0xc746A9A9C18159be5C29CaF74628c1EbdAEeC6E0", + }, + ], + salt: "292406351601814894369438777478694694865", + extraParams: "0x01", + nonce: 0, +}; + +const blurIoBid = { + trader: "0x900175B45Dcc84C23Cf597d5C3E766108CeA5bB0", + side: 0, + matchingPolicy: "0x0000000000daB4A563819e8fd93dbA3b25BC3495", + collection: "0x90c70Dc9f3FDa4a1D78a2B7D90CA087088355717", + tokenId: "12743", + amount: "1", + paymentToken: "0x0000000000000000000000000000000000000000", + price: "800000000000000000", + listingTime: "1677736151", + expirationTime: "1678340951", + fees: [ + { + rate: "500", + recipient: "0xc746A9A9C18159be5C29CaF74628c1EbdAEeC6E0", + }, + ], + salt: "292406351601814894369438777478694694865", + extraParams: "0x01", + nonce: 0, +}; + +const blurIoCollectionBid = { + trader: "0x377e19e0d6525f5fabd565bc47dc4e5fc8bafb01", + side: "0", + matchingPolicy: "0x0000000000b92d5d043faf7cecf7e2ee6aaed232", + collection: "0xf048cbaad26c1a35e7a04e126fdeb9c8045e676b", + tokenId: "0", + amount: "1", + paymentToken: "0x0000000000a39bb272e79075ade125fd351887ac", + price: "10000000000000000", + listingTime: "1678461048", + expirationTime: "1709997048", + fees: [], + salt: "315013322803695493589111125446364985004", + extraParams: "0x01", + nonce: "0", +}; + +Object.freeze(blurIoListing); +Object.freeze(blurIoBid); +Object.freeze(blurIoCollectionBid); + +export { blurIoListing, blurIoBid, blurIoCollectionBid }; diff --git a/test/visualizer/blur-io/index.test.ts b/test/visualizer/blur-io/index.test.ts new file mode 100644 index 0000000..a597883 --- /dev/null +++ b/test/visualizer/blur-io/index.test.ts @@ -0,0 +1,128 @@ +import { BlurIoOrder } from "../../../src/types/blur"; +import visualize from "../../../src/visualizer"; +import blurIo from "../../../src/visualizer/blur-io"; +import { blurIoBid, blurIoCollectionBid, blurIoListing } from "./data"; + +describe("blur.io", () => { + const blurIoDomain = { + name: "Blur Exchange", + version: "1.1", + chainId: "1", + verifyingContract: "0x000000000000ad05ccc4f10045630fb830b95127", + }; + + it("should revert if blurIo module called directly with wrong domain", () => { + expect(() => { + blurIo.visualize(blurIoListing, { ...blurIoDomain, chainId: "2" }); + }).toThrow("wrong blur.io domain"); + }); + + it("should revert unsupported matching policy", async () => { + await expect( + visualize({ ...blurIoListing, matchingPolicy: "0x0" }, blurIoDomain) + ).rejects.toThrow("unrecognized blur.io matching policy"); + }); + + it("should successfully visualize sell order", async () => { + const result = await visualize(blurIoListing, blurIoDomain); + expect(result).toEqual({ + protocol: "BLUR_IO_MARKETPLACE", + assetIn: [ + { + address: "0x0000000000000000000000000000000000000000", + type: "NATIVE", + amounts: ["760000000000000000"], + }, + ], + assetOut: [ + { + address: "0x90c70Dc9f3FDa4a1D78a2B7D90CA087088355717", + type: "ERC721", + id: "12743", + amounts: ["1"], + }, + ], + liveness: { from: 1677736151000, to: 1678340951000 }, + }); + }); + + it("should successfully visualize bid order", async () => { + const result = await visualize(blurIoBid, blurIoDomain); + + expect(result).toEqual({ + protocol: "BLUR_IO_MARKETPLACE", + assetIn: [ + { + address: "0x90c70Dc9f3FDa4a1D78a2B7D90CA087088355717", + type: "ERC721", + id: "12743", + amounts: ["1"], + }, + ], + assetOut: [ + { + address: "0x0000000000000000000000000000000000000000", + type: "NATIVE", + amounts: ["800000000000000000"], + }, + ], + liveness: { from: 1677736151000, to: 1678340951000 }, + }); + }); + + it("should successfully visualize collection bid order with pool ETH", async () => { + const result = await visualize(blurIoCollectionBid, blurIoDomain); + + expect(result).toEqual({ + protocol: "BLUR_IO_MARKETPLACE", + assetIn: [ + { + address: "0xf048cbaad26c1a35e7a04e126fdeb9c8045e676b", + type: "ERC721", + amounts: ["1"], + }, + ], + assetOut: [ + { + address: "0x0000000000000000000000000000000000000000", + type: "NATIVE", + amounts: ["10000000000000000"], + }, + ], + liveness: { from: 1678461048000, to: 1709997048000 }, + }); + }); + + it("should successfully visualize bid order with ERC20 token", async () => { + const result = await visualize( + { ...blurIoBid, paymentToken: "0xSomeERC20" }, + blurIoDomain + ); + + expect(result).toEqual({ + protocol: "BLUR_IO_MARKETPLACE", + assetIn: [ + { + address: "0x90c70Dc9f3FDa4a1D78a2B7D90CA087088355717", + type: "ERC721", + id: "12743", + amounts: ["1"], + }, + ], + assetOut: [ + { + address: "0xSomeERC20", + type: "ERC20", + amounts: ["800000000000000000"], + }, + ], + liveness: { from: 1677736151000, to: 1678340951000 }, + }); + }); + + it("should revert if wrong side", async () => { + await expect(visualize({ ...blurIoListing, side: 3 }, blurIoDomain)).rejects.toThrow( + "unrecognized blur.io order side" + ); + }); +});