@sx-bet/sportx-js
Version:
Provides an easy to use API to interact with the SportX relayer.
585 lines • 27.6 kB
JavaScript
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