Skip to content

Commit

Permalink
Merge c9ebf41 into c06980d
Browse files Browse the repository at this point in the history
  • Loading branch information
fzavalia committed May 9, 2022
2 parents c06980d + c9ebf41 commit 365fdd3
Show file tree
Hide file tree
Showing 6 changed files with 379 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=
236 changes: 236 additions & 0 deletions src/Rarity/Rarity.router.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
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, Rarity } 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())

let rarities: RarityFragment[]

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

rarities = [
{
id: 'common',
name: 'common',
price: '10000000000000000000',
maxSupply: '100000',
},
{
id: 'epic',
name: 'epic',
price: '10000000000000000000',
maxSupply: '1000',
},
{
id: 'legendary',
name: 'legendary',
price: '10000000000000000000',
maxSupply: '100',
},
{
id: 'mythic',
name: 'mythic',
price: '10000000000000000000',
maxSupply: '10',
},
{
id: 'rare',
name: 'rare',
price: '10000000000000000000',
maxSupply: '5000',
},
{
id: 'uncommon',
name: 'uncommon',
price: '10000000000000000000',
maxSupply: '10000',
},
{
id: 'unique',
name: 'unique',
price: '10000000000000000000',
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.map<Rarity>((r) => ({ ...r, currency: Currency.MANA })),
})
})
})

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

describe('when the currency query param is USD', () => {
it('should return a list of rarities obtained from the collectionAPI', async () => {
const { body } = await server
.get(buildURL('/rarities', { currency: Currency.USD }))
.expect(200)

expect(body).toEqual({
ok: true,
data: rarities.map<Rarity>((r) => ({
...r,
currency: Currency.USD,
})),
})
})
})

describe('when the currency query param is MANA', () => {
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: '4000000000000000000',
})
}

const { body } = await server
.get(buildURL('/rarities', { currency: Currency.MANA }))
.expect(200)

expect(body).toEqual({
ok: true,
data: rarities.map((r) => ({
...r,
price: '4000000000000000000',
currency: Currency.MANA,
})),
})
})

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

const { body } = await server
.get(buildURL('/rarities', { currency: Currency.MANA }))
.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)
})

describe('when the currency query param is MANA', () => {
it('should return the rarity with its price in MANA', async () => {
mockGetRarityFromBlockchain.mockResolvedValueOnce({
...rarities[0],
price: '4000000000000000000',
})

const { body } = await server
.get(buildURL('/rarities/common', { currency: Currency.MANA }))
.expect(200)

expect(body).toEqual({
ok: true,
data: {
...rarities[0],
price: '4000000000000000000',
currency: Currency.MANA,
},
})
})
})

describe('when the currency query param is USD', () => {
it('should return the rarity with its price in USD', async () => {
const { body } = await server
.get(buildURL('/rarities/common', { currency: Currency.USD }))
.expect(200)

expect(body).toEqual({
ok: true,
data: {
...rarities[0],
currency: Currency.USD,
},
})
})

describe('when the name provided is invalid', () => {
it('should return the rarity with its price in USD', async () => {
const { body } = await server
.get(buildURL('/rarities/invalid', { currency: Currency.USD }))
.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))
}

async getRarities() {
getRarities = async (req: Request): Promise<Rarity[]> => {
const rarities = await collectionAPI.fetchRarities()
return rarities

// If the server is still using the old rarities contract.
// Return rarities as they have been always returned
if (!isUsingRaritiesWithOracle()) {
return rarities.map((rarity) => ({ ...rarity, currency: Currency.MANA }))
}

const inUSD = req.query.currency === Currency.USD

// If the prices were requested in USD, just return the data from the graph.
if (inUSD) {
return rarities.map((rarity) => ({ ...rarity, currency: Currency.USD }))
}

// If not, query the blockchain to obtain the converted rarity prices in MANA
// from USD.
const blockchainRarities = await Promise.all(
rarities.map((rarity) => this.getRarityFromBlockchain(rarity.id))
)

const blockchainRaritiesMap = new Map(
blockchainRarities.map((rarity) => [rarity.id, rarity])
)

return rarities.map((rarity) => ({
...rarity,
price: blockchainRaritiesMap.get(rarity.id)!.price,
currency: Currency.MANA,
}))
}

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

const name = req.params.name

const inUSD = req.query.currency === Currency.USD

// If price is requested in USD, get rarities from the graph and return the data as is from
// the rarity with the given name.
if (inUSD) {
const rarities = await collectionAPI.fetchRarities()

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

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

return { ...rarity, currency: Currency.USD }
}

// If not, get the price converted in MANA from the blockchain for that given rarity.
const rarity = await this.getRarityFromBlockchain(name)

return { ...rarity, currency: Currency.MANA }
}

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(),
}
}
}
8 changes: 8 additions & 0 deletions src/Rarity/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RarityFragment } from '../ethereum/api/fragments'

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

export type Rarity = RarityFragment & { currency: Currency }
Loading

0 comments on commit 365fdd3

Please sign in to comment.