UNPKG

@sx-bet/sportx-js

Version:

Provides an easy to use API to interact with the SportX relayer.

585 lines 27.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { BigNumber } from "@ethersproject/bignumber"; import { isHexString } from "@ethersproject/bytes"; import { AddressZero, MaxUint256 } from "@ethersproject/constants"; import { Contract } from "@ethersproject/contracts"; import { StaticJsonRpcProvider } from "@ethersproject/providers"; import { randomBytes } from "@ethersproject/random"; import { Wallet } from "@ethersproject/wallet"; import * as ably from "ably"; import fetch from "cross-fetch"; import debug from "debug"; import ethSigUtil from "eth-sig-util"; import queryString from "query-string"; import ChildERC20 from "./artifacts/ChildERC20.json"; import ChildToken from "./artifacts/ChildToken.json"; import { CHAIN_IDS, DEFAULT_RPC_URLS, EIP712_FILL_HASHER_ADDRESSES, EIP712_VERSION, Environments, Networks, RELAYER_HTTP_ENDPOINTS, RELAYER_TIMEOUT, RELAYER_URLS, TOKEN_ADDRESSES, TOKEN_TRANSFER_PROXY_ADDRESS, } from "./constants"; import { APIError } from "./errors/api_error"; import { APISchemaError } from "./errors/schema_error"; import { convertToContractOrder } from "./utils/convert"; import { tryParseJson } from "./utils/misc"; import { getNetwork } from "./utils/networks"; import { getCancelAllOrdersEIP712Payload, getCancelOrderEIP712Payload, getCancelOrderEventsEIP712Payload, getFillOrderEIP712Payload, getOrderHash, getOrderSignature, } from "./utils/signing"; import { isAddress, isPositiveBigNumber, validateIFillDetailsMetadata, validateIGetPendingBetsRequest, validateIGetTradesRequest, validateINewOrderSchema, validateISignedRelayerMakerOrder, } from "./utils/validation"; class SportX { constructor(env, customProviderUrl, privateKey, customProvider, apiUrl, apiKey) { this.initialized = false; this.debug = debug("sportx-js"); this.baseTokenWrappers = {}; if (!Object.values(Environments).includes(env)) { throw new Error(`Invalid environment: ${env}`); } this.environment = env; this.network = getNetwork(this.environment); let providerUrl; providerUrl = DEFAULT_RPC_URLS[env]; if (customProviderUrl) { providerUrl = customProviderUrl; } if (privateKey && !isHexString(privateKey)) { throw new Error(`${privateKey} is not a valid private key.`); } else if (privateKey) { this.provider = new StaticJsonRpcProvider(providerUrl, CHAIN_IDS[this.network]); this.signingWallet = new Wallet(privateKey).connect(this.provider); this.privateKey = privateKey; } else if (customProvider) { this.signingWallet = customProvider.getSigner(0); this.provider = customProvider; } else { throw new Error(`Neither privateKey nor both providers provided.`); } this.apiKey = apiKey || ""; this.relayerUrl = apiUrl || RELAYER_URLS[env]; } cancelOrder(orderHashes) { return __awaiter(this, void 0, void 0, function* () { this.debug("cancelOrder"); if (!Array.isArray(orderHashes)) { throw new APISchemaError("orderHashes is not an array"); } if (!orderHashes.every((hash) => isHexString(hash))) { throw new APISchemaError("orderHashes has some invalid order hashes."); } const salt = `0x${Buffer.from(randomBytes(32)).toString("hex")}`; const timestamp = Math.floor(new Date().getTime() / 1000); const cancelOrderPayload = getCancelOrderEIP712Payload(orderHashes, salt, timestamp, this.sidechainChainId); this.debug("Signing payload"); this.debug(cancelOrderPayload); const signature = yield this.getEip712Signature(cancelOrderPayload); const payload = { signature, orderHashes, salt, maker: yield this.signingWallet.getAddress(), timestamp, }; this.debug("Cancel order payload"); this.debug(payload); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.CANCEL_ORDERS}`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't cancel orders."); this.debug("Relayer response"); this.debug(result); return result; }); } cancelAllOrders() { return __awaiter(this, void 0, void 0, function* () { this.debug("cancelAllOrders"); const salt = `0x${Buffer.from(randomBytes(32)).toString("hex")}`; const timestamp = Math.floor(new Date().getTime() / 1000); const cancelOrderPayload = getCancelAllOrdersEIP712Payload(salt, timestamp, this.sidechainChainId); this.debug("Signing payload"); this.debug(cancelOrderPayload); const signature = yield this.getEip712Signature(cancelOrderPayload); const payload = { signature, salt, maker: yield this.signingWallet.getAddress(), timestamp, }; this.debug("Cancel all orders payload"); this.debug(payload); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.CANCEL_ALL_ORDERS}`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't cancel orders."); this.debug("Relayer response"); this.debug(result); return result; }); } cancelOrdersByEvent(sportXeventId) { return __awaiter(this, void 0, void 0, function* () { this.debug("cancelOrderByEvent"); if (typeof sportXeventId !== "string") { throw new APISchemaError("sportXeventId is not a string"); } const salt = `0x${Buffer.from(randomBytes(32)).toString("hex")}`; const timestamp = Math.floor(new Date().getTime() / 1000); const cancelOrderPayload = getCancelOrderEventsEIP712Payload(sportXeventId, salt, timestamp, this.sidechainChainId); this.debug("Signing payload"); this.debug(cancelOrderPayload); const signature = yield this.getEip712Signature(cancelOrderPayload); const payload = { signature, sportXeventId, salt, maker: yield this.signingWallet.getAddress(), timestamp, }; this.debug("Cancel order event payload"); this.debug(payload); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.CANCEL_EVENT_ORDERS}`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't cancel orders."); this.debug("Relayer response"); this.debug(result); return result; }); } getPopularMarkets() { return __awaiter(this, void 0, void 0, function* () { this.debug("getPopularMarkets"); const url = `${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.POPULAR}`; const response = yield fetch(url); const result = yield this.tryParseResponse(response, "Can't fetch active markets"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } getRealtimeConnection() { return this.ably; } init() { return __awaiter(this, void 0, void 0, function* () { if (this.initialized) { throw new Error("Already initialized"); } this.ably = new ably.Realtime.Promise({ authUrl: `${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.USER_TOKEN}`, }); yield new Promise((resolve, reject) => { this.ably.connection.on("connected", () => { resolve(); }); setTimeout(() => reject(), RELAYER_TIMEOUT); }); this.metadata = yield this.getMetadata(); const sidechainNetwork = yield this.provider.getNetwork(); this.sidechainChainId = sidechainNetwork.chainId; this.verifyChainIds(); Object.entries(TOKEN_ADDRESSES[this.network]).map(([, address]) => __awaiter(this, void 0, void 0, function* () { this.baseTokenWrappers[address] = new Contract(address, ChildERC20.abi, this.provider); })); this.initialized = true; this.debug("Initialized"); }); } getActiveLeagues() { return __awaiter(this, void 0, void 0, function* () { this.debug("getLeagues"); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.ACTIVE_LEAGUES}`); const result = yield this.tryParseResponse(response, "Can't fetch leagues"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } getMetadata() { return __awaiter(this, void 0, void 0, function* () { this.debug("getMetadata"); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.METADATA}`); const result = yield this.tryParseResponse(response, "Can't fetch metadata"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } getLeagues() { return __awaiter(this, void 0, void 0, function* () { this.debug("getLeagues"); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.LEAGUES}`); const result = yield this.tryParseResponse(response, "Can't fetch leagues"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } getSports() { return __awaiter(this, void 0, void 0, function* () { this.debug("getSports"); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.SPORTS}`); const result = yield this.tryParseResponse(response, "Can't fetch sports"); const { data } = result; return data; }); } getLiveScores(eventIds) { return __awaiter(this, void 0, void 0, function* () { this.debug("getLiveScores"); if (!Array.isArray(eventIds) || !eventIds.every((eventId) => typeof eventId === "string")) { throw new APISchemaError("eventIds is not an array of strings"); } const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.LIVE_SCORES}`, { method: "POST", body: JSON.stringify({ sportXEventIds: eventIds }), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't get live scores"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } getActiveMarkets(mainLinesOnly, eventId, leagueId, liveOnly, betGroup) { return __awaiter(this, void 0, void 0, function* () { this.debug("getActiveMarkets"); const qs = queryString.stringify(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (mainLinesOnly !== undefined && { onlyMainLine: mainLinesOnly })), (leagueId !== undefined && { leagueId })), (eventId !== undefined && { eventId })), (liveOnly !== undefined && { liveOnly })), (betGroup !== undefined && { betGroup }))); const url = `${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.ACTIVE_MARKETS}?${qs}`; const response = yield fetch(url); const result = yield this.tryParseResponse(response, "Can't fetch active markets"); this.debug("Relayer response"); this.debug(result); const { data: { markets }, } = result; return markets; }); } marketLookup(marketHashes) { return __awaiter(this, void 0, void 0, function* () { this.debug("marketLookup"); const payload = { marketHashes, }; if (!Array.isArray(marketHashes) || !marketHashes.every((hash) => isHexString(hash))) { throw new APISchemaError("marketHashes is not a hex string "); } const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.HISTORICAL_MARKETS}`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't lookup markets"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } newOrder(orders) { return __awaiter(this, void 0, void 0, function* () { this.debug("newOrder"); const validation = orders.map(validateINewOrderSchema); validation.forEach((val) => { if (val !== "OK") { throw new APISchemaError(val); } }); const walletAddress = yield this.signingWallet.getAddress(); const apiOrders = yield Promise.all(orders.map((order) => __awaiter(this, void 0, void 0, function* () { const bigNumBetSize = BigNumber.from(order.totalBetSize); const salt = BigNumber.from(randomBytes(32)).toString(); const apiMakerOrder = { marketHash: order.marketHash, maker: walletAddress, totalBetSize: bigNumBetSize.toString(), percentageOdds: order.percentageOdds, expiry: 2209006800, apiExpiry: order.expiry, executor: this.metadata.executorAddress, baseToken: order.baseToken, salt, isMakerBettingOutcomeOne: order.isMakerBettingOutcomeOne, }; const signature = yield getOrderSignature(apiMakerOrder, this.signingWallet); const signedApiMakerOrder = Object.assign(Object.assign({}, apiMakerOrder), { signature }); return signedApiMakerOrder; }))); this.debug(`New signed orders`); this.debug(apiOrders); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.NEW_ORDER}`, { method: "POST", body: JSON.stringify({ orders: apiOrders }), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't submit new order"); this.debug("Relayer response"); this.debug(result); return result; }); } fillOrders(orders, takerAmounts, fillDetailsMetadata, affiliateAddress, approveProxyPayload) { return __awaiter(this, void 0, void 0, function* () { this.debug("fillOrders"); orders.forEach((order) => { const validation = validateISignedRelayerMakerOrder(order); if (validation !== "OK") { this.debug("One of the orders is malformed"); throw new APISchemaError(validation); } }); if (affiliateAddress && !isAddress(affiliateAddress)) { this.debug("Affiliate address is malformed"); throw new APISchemaError("Affiliate address malformed."); } if (fillDetailsMetadata) { const validation = validateIFillDetailsMetadata(fillDetailsMetadata); if (validation !== "OK") { this.debug("Metadata malformed"); throw new APISchemaError(validation); } } if (!Array.isArray(takerAmounts)) { throw new APISchemaError("takerAmounts is not an array"); } if (!takerAmounts.every((amount) => isPositiveBigNumber(amount))) { throw new APISchemaError("takerAmounts has some invalid number strings"); } const fillSalt = BigNumber.from(randomBytes(32)); const solidityOrders = orders.map(convertToContractOrder); const orderHashes = solidityOrders.map(getOrderHash); const finalFillDetailsMetadata = fillDetailsMetadata || { action: "N/A", market: "N/A", betting: "N/A", stake: "N/A", odds: "N/A", returning: "N/A", }; const fillDetails = Object.assign(Object.assign({}, finalFillDetailsMetadata), { fills: { orders: orders.map(convertToContractOrder), makerSigs: orders.map((order) => order.signature), takerAmounts: takerAmounts.map(BigNumber.from), fillSalt, beneficiary: AddressZero, } }); const fillOrderPayload = getFillOrderEIP712Payload(fillDetails, this.sidechainChainId, EIP712_VERSION[this.environment], EIP712_FILL_HASHER_ADDRESSES[this.environment]); this.debug(`EIP712 payload`); this.debug(fillOrderPayload); const takerSignature = yield this.getEip712Signature(fillOrderPayload); const payload = Object.assign(Object.assign({ orderHashes, takerAmounts, taker: yield this.signingWallet.getAddress(), takerSig: takerSignature, fillSalt: fillSalt.toString() }, finalFillDetailsMetadata), { affiliateAddress, approveProxyPayload }); if (approveProxyPayload) { this.debug(`approveProxyPayload`); this.debug(approveProxyPayload); } this.debug("Meta fill payload"); this.debug(payload); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.FILL_ORDERS}`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't fill orders."); this.debug("Relayer response"); this.debug(result); return result; }); } getPendingOrFailedBets(request) { return __awaiter(this, void 0, void 0, function* () { this.debug("getRecentPendingBets"); const validation = validateIGetPendingBetsRequest(request); if (validation !== "OK") { throw new APISchemaError(validation); } const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.PENDING_BETS}`, { method: "POST", body: JSON.stringify(request), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't get recent pending bets"); this.debug("Relayer response"); this.debug(result); const { data: { bets }, } = result; const pendingBets = bets; return pendingBets; }); } getTrades(tradeRequest) { return __awaiter(this, void 0, void 0, function* () { this.debug("getTrades"); const validation = validateIGetTradesRequest(tradeRequest); if (validation !== "OK") { throw new APISchemaError(validation); } const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.TRADES}`, { method: "POST", body: JSON.stringify(tradeRequest), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't get trades"); this.debug("Relayer response"); this.debug(result); const { data } = result; return data; }); } getOrders(marketHashes, maker, baseToken) { return __awaiter(this, void 0, void 0, function* () { this.debug("getOrders"); if (marketHashes && !marketHashes.every((hash) => isHexString(hash))) { throw new APISchemaError(`One of the supplied market hashes is not a valid hex string.`); } if (maker && !isAddress(maker)) { throw new APISchemaError(`maker is not a valid address`); } if (baseToken && !isAddress(baseToken)) { throw new APISchemaError(`baseToken is not a valid address`); } const payload = Object.assign(Object.assign(Object.assign({}, (marketHashes && { marketHashes })), (maker && { maker })), (baseToken && { baseToken })); const response = yield fetch(`${this.relayerUrl}${RELAYER_HTTP_ENDPOINTS.ORDERS}`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", "X-Api-Key": this.apiKey, }, }); const result = yield this.tryParseResponse(response, "Can't get orders"); this.debug("Relayer response"); this.debug(result); const { data } = result; const orders = data; return orders; }); } approveSportXContracts(token) { return __awaiter(this, void 0, void 0, function* () { const tokenContract = new Contract(token, ChildToken.abi, this.signingWallet); const approvalTxn = yield tokenContract.approve(TOKEN_TRANSFER_PROXY_ADDRESS[this.environment], MaxUint256); const receipt = yield approvalTxn.wait(); this.debug("Approval Txn reciept"); this.debug(receipt); return receipt; }); } getEip712Signature(payload) { return __awaiter(this, void 0, void 0, function* () { if (this.privateKey) { const bufferPrivateKey = Buffer.from(this.privateKey.substring(2), "hex"); const signature = ethSigUtil.signTypedData_v4(bufferPrivateKey, { data: payload }); return signature; } else if (this.provider._web3Provider.isMetaMask === true) { const walletAddress = yield this.signingWallet.getAddress(); const signature = yield this.provider.send("eth_signTypedData_v4", [walletAddress, JSON.stringify(payload)]); return signature; } else { const walletAddress = yield this.signingWallet.getAddress(); const signature = yield this.provider.send("eth_signTypedData", [ walletAddress, payload, ]); return signature; } }); } tryParseResponse(response, errorMessage) { return __awaiter(this, void 0, void 0, function* () { const textResponse = yield response.text(); const { result, valid } = tryParseJson(textResponse); if (valid && response.status !== 200) { this.debug(response.status); this.debug(response.statusText); this.debug(result); throw new APIError(result, `${errorMessage}. Response code: ${response.status}`); } else if (!valid) { throw new APIError(undefined, `Can't parse JSON ${textResponse}`); } else { return result; } }); } verifyChainIds() { if (this.environment === Environments.SxMainnet) { if (this.sidechainChainId !== CHAIN_IDS[Networks.SX_MAINNET]) { throw new Error(`Incorrect sidechain chain ID for production environment. Are you sure the passed sidechain provider is pointing to Polygon mainnet?`); } } else if (this.environment === Environments.SxToronto) { if (this.sidechainChainId !== CHAIN_IDS[Networks.SX_TORONTO]) { throw new Error(`Incorrect sidechain chain ID for sx_toronto environment. Are you sure the passed sidechain provider is pointing to Toronto?`); } } } } export function newSportX(sportXObj) { return __awaiter(this, void 0, void 0, function* () { const { env, customSidechainProviderUrl, privateKey, sidechainProvider, apiUrl, apiKey, } = sportXObj; const sportX = new SportX(env, customSidechainProviderUrl, privateKey, sidechainProvider, apiUrl, apiKey); yield sportX.init(); return sportX; }); } export function getValidEnv(env) { switch (env) { case Environments.SxMainnet: return Environments.SxMainnet; case Environments.SxStage: return Environments.SxStage; case Environments.SxToronto: return Environments.SxToronto; default: throw new Error(`Unknown environment ${env}`); } } //# sourceMappingURL=sportx.js.map