Skip to content

Commit

Permalink
Merge c488adc into c06980d
Browse files Browse the repository at this point in the history
  • Loading branch information
fzavalia committed May 9, 2022
2 parents c06980d + c488adc commit 047fc51
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .ci/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ export = async function main() {
name: 'COLLECTION_FACTORY_VERSION',
value: env === 'prd' || env === 'stg' ? '2' : '3',
},
{
name: 'FF_RARITIES_WITH_ORACLE',
value: '0',
},
],
hostname,
{
Expand Down
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ WAREHOUSE_CONTEXT_PREFIX=
OPEN_SEA_URL=
OPEN_SEA_API_KEY=
COLLECTION_FACTORY_VERSION=
FF_RARITIES_WITH_ORACLE=
203 changes: 203 additions & 0 deletions src/Rarity/Rarity.router.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import supertest from 'supertest'
import { buildURL } from '../../spec/utils'
import { collectionAPI } from '../ethereum/api/collection'
import { RarityFragment } from '../ethereum/api/fragments'
import { app } from '../server'
import { Currency } from './types'
import { getRarityFromBlockchain, isUsingRaritiesWithOracle } from './utils'

jest.mock('../ethereum/api/collection')
jest.mock('./utils')

const mockCollectionAPI = collectionAPI as jest.Mocked<typeof collectionAPI>
const mockIsUsingRaritiesWithOracle = isUsingRaritiesWithOracle as jest.MockedFunction<
typeof isUsingRaritiesWithOracle
>
const mockGetRarityFromBlockchain = getRarityFromBlockchain as jest.MockedFunction<
typeof getRarityFromBlockchain
>

const server = supertest(app.getApp())

const priceUsd = '10000000000000000000'
const priceMana = '4000000000000000000'

let rarities: RarityFragment[]

beforeEach(() => {
jest.clearAllMocks()

rarities = [
{
id: 'common',
name: 'common',
price: priceUsd,
maxSupply: '100000',
},
{
id: 'epic',
name: 'epic',
price: priceUsd,
maxSupply: '1000',
},
{
id: 'legendary',
name: 'legendary',
price: priceUsd,
maxSupply: '100',
},
{
id: 'mythic',
name: 'mythic',
price: priceUsd,
maxSupply: '10',
},
{
id: 'rare',
name: 'rare',
price: priceUsd,
maxSupply: '5000',
},
{
id: 'uncommon',
name: 'uncommon',
price: priceUsd,
maxSupply: '10000',
},
{
id: 'unique',
name: 'unique',
price: priceUsd,
maxSupply: '1',
},
]

mockCollectionAPI.fetchRarities.mockResolvedValueOnce(rarities)
})

describe('when fetching all rarities', () => {
describe('when rarities with oracle feature flag is disabled', () => {
beforeEach(() => {
mockIsUsingRaritiesWithOracle.mockReturnValueOnce(false)
})

it('should return a list of rarities obtained from the collectionAPI', async () => {
const { body } = await server.get(buildURL('/rarities')).expect(200)

expect(body).toEqual({
ok: true,
data: rarities,
})
})
})

describe('when rarities with oracle feature flag is enabled', () => {
beforeEach(() => {
mockIsUsingRaritiesWithOracle.mockReturnValueOnce(true)
})

it('should return a list of rarities with the price converted to MANA from USD', async () => {
for (const r of rarities) {
mockGetRarityFromBlockchain.mockResolvedValueOnce({
...r,
price: priceMana,
})
}

const { body } = await server.get(buildURL('/rarities')).expect(200)

expect(body).toEqual({
ok: true,
data: rarities.map((r) => ({
...r,
prices: {
[Currency.MANA]: priceMana,
[Currency.USD]: priceUsd,
},
})),
})
})

describe('when fetching a rarity from the blockchain fails', () => {
it('should fail with a could not fetch from blockchain error', async () => {
mockGetRarityFromBlockchain.mockImplementation(() =>
Promise.reject(new Error('Atahualpa Yupanqui'))
)

const { body } = await server.get(buildURL('/rarities')).expect(404)

expect(body).toEqual({
ok: false,
error: 'Could not fetch rarity from blockchain',
data: {
name: 'common',
},
})
})
})
})
})

describe('when fetching a single rarity by name', () => {
describe('when rarities with oracle feature flag is disabled', () => {
beforeEach(() => {
mockIsUsingRaritiesWithOracle.mockReturnValueOnce(false)
})

it('should fail with an endpoint not found', async () => {
const { body } = await server
.get(buildURL('/rarities/common'))
.expect(404)

expect(body).toEqual({
data: {},
error: 'Cannot GET /rarities/common',
ok: false,
})
})
})

describe('when rarities with oracle feature flag is enabled', () => {
beforeEach(() => {
mockIsUsingRaritiesWithOracle.mockReturnValueOnce(true)
})

it('should return the rarity with its price converted from USD to MANA', async () => {
mockGetRarityFromBlockchain.mockResolvedValueOnce({
...rarities[0],
price: priceMana,
})

const { body } = await server
.get(buildURL('/rarities/common'))
.expect(200)

expect(body).toEqual({
ok: true,
data: {
...rarities[0],
prices: {
[Currency.MANA]: priceMana,
[Currency.USD]: priceUsd,
},
},
})
})

describe('when the name provided is invalid', () => {
it('should fail with an error saying that the rarity could not be found', async () => {
const { body } = await server
.get(buildURL('/rarities/invalid'))
.expect(404)

expect(body).toEqual({
ok: false,
error: 'Rarity not found',
data: {
name: 'invalid',
},
})
})
})
})
})
97 changes: 91 additions & 6 deletions src/Rarity/Rarity.router.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,103 @@
import { server } from 'decentraland-server'

import { Request } from 'express'
import { Router } from '../common/Router'
import { collectionAPI } from '../ethereum/api/collection'
import { RarityFragment } from '../ethereum/api/fragments'
import { HTTPError, STATUS_CODES } from '../common/HTTPError'
import { isUsingRaritiesWithOracle, getRarityFromBlockchain } from './utils'
import { Currency, Rarity } from './types'

export class RarityRouter extends Router {
mount() {
/**
* Returns the available rarities
*/
// Returns the available rarities.
this.router.get('/rarities', server.handleRequest(this.getRarities))

// Returns a single rarity according to the rarity name provided.
this.router.get('/rarities/:name', server.handleRequest(this.getRarity))
}

getRarities = async (): Promise<Rarity[]> => {
const graphRarities = await collectionAPI.fetchRarities()

// If the server is still using the old rarities contract,
// return rarities as they have been always returned.
if (!isUsingRaritiesWithOracle()) {
return graphRarities
}

// Query the blockchain to obtain rarities with MANA prices converted from USD.
const blockchainRarities = await Promise.all(
graphRarities.map((rarity) => this.getRarityFromBlockchain(rarity.id))
)

// Convert the array into a Map for an easier lookup
const blockchainRaritiesMap = new Map(
blockchainRarities.map((rarity) => [rarity.name, rarity])
)

// Consolidate rarities obtained from the graph with rarities obtained
// from the blockchain.
return graphRarities.map((graphRarity) => {
// Not handling if the blockchain rarity is not present because that is checked when calling the
// this.getRarityFromBlockchain function.
const blockchainRarity = blockchainRaritiesMap.get(graphRarity.name)!

return {
...graphRarity,
prices: {
[Currency.MANA]: blockchainRarity.price,
[Currency.USD]: graphRarity.price,
},
}
})
}

async getRarities() {
getRarity = async (req: Request): Promise<Rarity> => {
if (!isUsingRaritiesWithOracle()) {
throw new HTTPError(`Cannot GET ${req.path}`, {}, STATUS_CODES.notFound)
}

const name = req.params.name

const rarities = await collectionAPI.fetchRarities()
return rarities

const graphRarity = rarities.find((r) => r.name === name)

if (!graphRarity) {
throw new HTTPError('Rarity not found', { name }, STATUS_CODES.notFound)
}

const blockchainRarity = await this.getRarityFromBlockchain(name)

return {
...graphRarity,
prices: {
[Currency.MANA]: blockchainRarity.price,
[Currency.USD]: graphRarity.price,
},
}
}

private getRarityFromBlockchain = async (
name: string
): Promise<RarityFragment> => {
let rarity: any

try {
rarity = await getRarityFromBlockchain(name)
} catch (e) {
throw new HTTPError(
'Could not fetch rarity from blockchain',
{ name },
STATUS_CODES.notFound
)
}

return {
id: rarity.name,
name: rarity.name,
price: rarity.price.toString(),
maxSupply: rarity.maxSupply.toString(),
}
}
}
15 changes: 15 additions & 0 deletions src/Rarity/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { RarityFragment } from '../ethereum/api/fragments'

export enum Currency {
MANA = 'MANA',
USD = 'USD',
}

/**
* Type returned by the Rarity router.
* The "prices" field is only defined when using the RaritiesWithOracle contract.
* The "prices" field contains the different prices of a rarity in different currencies.
*/
export type Rarity = RarityFragment & {
prices?: Record<Currency, string>
}
39 changes: 39 additions & 0 deletions src/Rarity/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { env } from 'decentraland-commons'
import { ContractName, getContract } from 'decentraland-transactions'
import { ethers } from 'ethers'
import { getMappedChainIdForCurrentChainName } from '../ethereum/utils'

export function isUsingRaritiesWithOracle(): boolean {
return env.get<string | undefined>('FF_RARITIES_WITH_ORACLE') === '1'
}

export function getRarityFromBlockchain(
name: string
): Promise<{ name: string; price: string; maxSupply: string }> {
const chainId = getMappedChainIdForCurrentChainName()

const raritiesWithOracle = getContract(
ContractName.RaritiesWithOracle,
chainId
)

const provider = new ethers.providers.JsonRpcProvider(getMaticRpcUrl())

const contract = new ethers.Contract(
raritiesWithOracle.address,
raritiesWithOracle.abi,
provider
)

return contract.getRarityByName(name)
}

function getMaticRpcUrl(): string {
const maticRpcUrl = env.get<string | undefined>('MATIC_RPC_URL')

if (!maticRpcUrl) {
throw new Error('MATIC_RPC_URL not defined')
}

return maticRpcUrl
}

0 comments on commit 047fc51

Please sign in to comment.