-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
379 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,3 +39,4 @@ WAREHOUSE_CONTEXT_PREFIX= | |
OPEN_SEA_URL= | ||
OPEN_SEA_API_KEY= | ||
COLLECTION_FACTORY_VERSION= | ||
FF_RARITIES_WITH_ORACLE= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
Oops, something went wrong.