-
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
353 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,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', | ||
}, | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
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)) | ||
} | ||
|
||
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(), | ||
} | ||
} | ||
} |
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,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> | ||
} |
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,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 | ||
} |