ccxt
Version:
1,016 lines (1,014 loc) • 193 kB
JavaScript
// ----------------------------------------------------------------------------
// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
// EDIT THE CORRESPONDENT .ts FILE INSTEAD
/// <reference lib="es2015" />
// ---------------------------------------------------------------------------
//
// Myriad Protocol CCXT Exchange adapter (https://myriad.markets)
//
// Hierarchy: Questions (events) → Markets (multi-chain, multi-outcome)
//
// Each market becomes one CCXT market with an outcomes list:
// market.id: {networkId}:{marketId}
// market.symbol: SLUG_SHORT
// outcomes[i].symbol: SLUG_SHORT:OUTCOME_LABEL
//
// Supports Abstract (2741), Linea (59144), BNB Chain (56).
//
// ---------------------------------------------------------------------------
import { keccak_256 as keccak } from '@noble/hashes/sha3.js';
import { secp256k1 } from '@noble/curves/secp256k1.js';
import Exchange from '../abstract/prediction/myriad.js';
import { ecdsa } from '../base/functions/crypto.js';
import { ArrayCache, ArrayCacheByOutcomeById } from '../base/ws/Cache.js';
import { Precise } from '../base/Precise.js';
import { ArgumentsRequired, NotSupported, ExchangeError, InvalidOrder, InsufficientFunds, OrderNotFound, BadSymbol, AuthenticationError, RateLimitExceeded, BadRequest } from '../base/errors.js';
// ---------------------------------------------------------------------------
/**
* @class myriad
* @augments Exchange
*/
export default class myriad extends Exchange {
describe() {
return this.deepExtend(super.describe(), {
'id': 'myriad',
'name': 'Myriad',
'countries': [],
'rateLimit': 200,
'certified': false,
'pro': true,
'has': {
'CORS': undefined,
'spot': false,
'margin': false,
'swap': false,
'future': false,
'option': false,
'cancelAllOrders': true,
'cancelOrder': true,
'cancelOrders': true,
'createOrder': true,
'createOrders': true,
'editOrder': true,
'fetchBalance': true,
'fetchCanceledOrders': true,
'fetchClosedOrders': true,
'fetchCurrencies': false,
'fetchEvent': true,
'fetchEvents': true,
'fetchMarkets': true,
'fetchMyTrades': true,
'fetchOHLCV': true,
'fetchOpenOrders': true,
'fetchOrder': true,
'fetchOrderBook': true,
'fetchOrders': true,
'fetchPositions': true,
'fetchTicker': true,
'fetchTickers': true,
'fetchTrades': true,
'fetchTradingFee': true,
'prediction': true,
'watchMyTrades': true,
'watchOHLCV': true,
'watchOrderBook': true,
'watchOrders': true,
'watchPositions': true,
'watchTicker': true,
'watchTickers': true,
'watchTrades': true,
},
'timeframes': {
// Myriad maps timeframes to price_chart bucket keys
'1m': '24h',
'5m': '24h',
'15m': '7d',
'1h': '7d',
'6h': '30d',
'1d': '30d',
},
'urls': {
'logo': 'https://github.com/user-attachments/assets/a393c885-99e8-4943-897d-ebc0555c3773',
'api': {
'myriad': 'https://api-v2.myriadprotocol.com',
'ws': 'wss://ws.myriadprotocol.com/ws',
},
'test': {
'myriad': 'https://api-v2.staging.myriadprotocol.com',
'ws': 'wss://ws.staging.myriadprotocol.com/ws',
},
'www': 'https://myriad.markets',
'doc': ['https://docs.myriad.markets'],
},
'api': {
'myriad': {
'public': {
'get': {
'questions': 1,
'questions/{id}': 1,
'markets': 1,
'markets/{id}': 1,
'markets/{networkId}/{id}': 1,
'markets/{id}/events': 1,
'markets/{id}/orderbook': 1,
'markets/{id}/trades': 1,
'markets/{id}/holders': 1,
'markets/{id}/referrals': 1,
'events': 1,
'orders': 1,
'orders/{hash}': 1,
'users/{address}/events': 1,
'users/{address}/referrals': 1,
'users/{address}/portfolio': 1,
'users/{address}/markets': 1,
'tags': 1,
'topics': 1,
},
'post': {
'markets/quote': 1,
'markets/claim': 1,
'orders': 1,
'orders/cancel-batch': 1,
'orders/cancel-all': 1,
'positions/split': 1,
'positions/merge': 1,
'positions/redeem': 1,
'positions/redeem-voided': 1,
'positions/neg-risk/split': 1,
'positions/neg-risk/merge': 1,
},
'delete': {
'orders/{hash}': 1,
},
},
'private': {
'post': {
'markets/quote_with_fee': 1,
},
},
},
},
'requiredCredentials': {
// apiKey is the optional x-api-key header (higher rate limits); trading needs the privateKey
'apiKey': false,
'secret': false,
'walletAddress': false,
'privateKey': true,
},
'fees': {
'trading': {
'tierBased': false,
'percentage': true,
'maker': 0.01,
'taker': 0.01,
},
},
'exceptions': {
'exact': {
'Order not found': OrderNotFound,
'Market not found': BadSymbol,
'Invalid order payload': InvalidOrder,
},
'broad': {
'Insufficient': InsufficientFunds,
'allowance': InsufficientFunds,
'not found': OrderNotFound,
'Unauthorized': AuthenticationError,
'Forbidden': AuthenticationError,
'rate limit': RateLimitExceeded,
'Too many requests': RateLimitExceeded,
'expired': InvalidOrder,
'closed': InvalidOrder,
'resolved': InvalidOrder,
'Invalid': InvalidOrder,
},
},
'options': {
'defaultFetchMarketsLimit': 50,
'defaultFetchEventsLimit': 50,
// allow unscoped fetchEvents() for this venue; we fetch bounded open lists
// from both markets and questions and merge them with overlap filtering
'allowUnscopedFetchEvents': true,
'defaultMarketStatus': 'open', // 'open' | 'closed' | 'resolved'
'defaultTradingModel': 'all', // 'amm' | 'ob' | 'all' — markets listing includes both models
// network used for order-book trading when a market does not pin one (OB lives on BNB Chain)
'defaultNetworkId': '56',
// EIP-712 domain for order-book order/cancel signing (gasless CLOB)
'obDomainName': 'MyriadCTFExchange',
'obDomainVersion': '1',
'networks': {
'2741': 'Abstract',
'59144': 'Linea',
'56': 'BNB Chain',
},
// on-chain config per network id (= chain id); predictionMarket settles the AMM path,
// obExchangeAddress is the EIP-712 verifyingContract for the gasless order book.
// rpcUrl can be overridden via params.rpcUrl or options.chains[networkId].rpcUrl
'chains': {
'56': { 'rpcUrl': 'https://bsc-dataseed.binance.org/', 'predictionMarket': '0x39E66eE6b2ddaf4DEfDEd3038E0162180dbeF340', 'obExchangeAddress': '0xa0b6f8ef8EdB64f395018D1933f2273Ce9f0f16A', 'obConditionalTokens': '0x6413734f92248D4B29ae35883290BD93212654Dc', 'collateralToken': '0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d', 'collateralCurrency': 'USD1', 'collateralDecimals': 18 },
'2741': { 'rpcUrl': 'https://api.mainnet.abs.xyz', 'predictionMarket': '0x3e0F5F8F5Fb043aBFA475C0308417Bf72c463289' },
'59144': { 'rpcUrl': 'https://rpc.linea.build', 'predictionMarket': '0x39e66ee6b2ddaf4defded3038e0162180dbef340' },
},
},
});
}
/**
* @method
* @name myriad#fetchMarkets
* @description retrieves data on all markets for myriad, each prediction market becomes one market with its outcome tokens listed under the outcomes key
* @see https://docs.myriad.markets/builders/myriad-api-reference
* @param {object} [params] extra exchange-specific parameters
* @param {string} [params.query] a single search term used to filter the fetched markets
* @param {string[]} [params.queries] multiple search terms (alternative to query)
* @param {string} [params.state] 'open', 'closed' or 'resolved', the state of the markets to fetch, defaults to 'open'
* @param {int} [params.limit] max number of markets to collect (defaults to options.fetchMarketsLimit, 1000); stops the pagination once reached
* @returns {object[]} an array of objects representing market data
*/
async fetchMarkets(params = {}) {
const queries = this.parseSearchQueries(params);
const rest = this.omit(params, ['query', 'queries']);
const queriesLength = queries.length;
let rawMarkets = [];
if (queriesLength > 0) {
rawMarkets = await this.fetchRawMarketsBySearch(queries, rest);
}
else {
rawMarkets = await this.fetchRawMarketsList(rest);
}
const flatMarkets = [];
const eventsDict = {};
for (let i = 0; i < rawMarkets.length; i++) {
const raw = rawMarkets[i];
const m = this.parseMyriadMarket(raw);
flatMarkets.push(m);
const ev = this.parseMarketToEvent(raw, m);
const evKey = this.safeString(ev, 'event');
if (evKey !== undefined) {
eventsDict[evKey] = ev;
}
}
this.events = eventsDict;
return flatMarkets;
}
/**
* @ignore
* @method
* @name myriad#fetchRawMarketsBySearch
* @description fetches raw myriad market objects matching the given search terms via the markets keyword filter
* @see https://docs.myriad.markets/builders/myriad-api-reference
* @param {string[]} queries search terms
* @param {object} [params] extra exchange-specific parameters
* @param {int} [params.limit] maximum number of markets per query, defaults to 50
* @param {string} [params.state] 'open', 'closed' or 'resolved', defaults to options.defaultMarketStatus
* @returns {object[]} an array of raw myriad market objects
*/
async fetchRawMarketsBySearch(queries, params = {}) {
const limit = this.safeInteger(params, 'limit', this.safeInteger(this.options, 'defaultFetchEventsLimit', 50));
const state = this.safeString(params, 'state', this.safeString(this.options, 'defaultMarketStatus', 'open'));
const rest = this.omit(params, ['limit', 'state']);
const seen = {};
const rawMarkets = [];
for (let i = 0; i < queries.length; i++) {
const q = queries[i];
const response = await this.myriadPublicGetMarkets(this.extend({
'keyword': q,
'state': state,
'limit': limit,
}, rest));
const foundList = this.safeList(response, 'data', response);
const found = (foundList !== undefined) ? foundList : [];
for (let j = 0; j < found.length; j++) {
const raw = found[j];
const networkId = this.safeString(raw, 'networkId');
const marketId = this.safeString(raw, 'id');
const key = networkId + ':' + marketId;
if (!(key in seen)) {
seen[key] = true;
rawMarkets.push(raw);
}
}
}
return rawMarkets;
}
/**
* @ignore
* @method
* @name myriad#fetchRawMarketsList
* @description fetches raw myriad market objects from the paginated markets listing
* @see https://docs.myriad.markets/builders/myriad-api-reference
* @param {object} [params] extra exchange-specific parameters
* @param {string} [params.state] 'open', 'closed' or 'resolved', defaults to options.defaultMarketStatus
* @returns {object[]} an array of raw myriad market objects
*/
async fetchRawMarketsList(params = {}) {
const limit = this.safeInteger(this.options, 'defaultFetchMarketsLimit', 50);
// scope the listing: without a search query loadMarkets would otherwise page through
// every open myriad market. Cap the total number of markets collected.
const maxMarkets = this.safeInteger(params, 'limit', this.safeInteger(this.options, 'fetchMarketsLimit', 1000));
const state = this.safeString2(params, 'state', 'status', this.safeString(this.options, 'defaultMarketStatus', 'open'));
// include both AMM and order-book markets so order-book trading methods can resolve their markets
const tradingModel = this.safeString2(params, 'tradingModel', 'trading_model', this.safeString(this.options, 'defaultTradingModel', 'all'));
const rest = this.omit(params, ['state', 'status', 'limit', 'tradingModel', 'trading_model']);
const allRawMarkets = [];
// track the running count with an explicit counter (avoids inline array .length / .slice,
// which the regex transpiler otherwise mistakes for string strlen()/mb_substr())
let collected = 0;
let page = 1;
while (true) {
const response = await this.myriadPublicGetMarkets(this.extend({
'state': state,
'limit': limit,
'page': page,
'trading_model': tradingModel,
}, rest));
const rawMarketsList = this.safeList(response, 'data', response);
const rawMarkets = (rawMarketsList !== undefined) ? rawMarketsList : [];
const rawMarketsLength = rawMarkets.length;
if (rawMarketsLength === 0) {
break;
}
for (let i = 0; i < rawMarketsLength; i++) {
if (collected < maxMarkets) {
allRawMarkets.push(rawMarkets[i]);
collected = this.sum(collected, 1);
}
}
page = this.sum(page, 1);
if (rawMarketsLength < limit || collected >= maxMarkets) {
break;
}
}
return allRawMarkets;
}
/**
* @method
* @name myriad#fetchEvent
* @description fetches a single prediction-market event by its market id, or orderbook slug
* @see https://docs.myriad.markets/builders/myriad-api-reference
* @param {string} id the market id, or orderbook slug
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} a [prediction event structure](https://docs.ccxt.com/#/?id=prediction-event-structure)
*/
async fetchEvent(id, params = {}) {
if (id.indexOf(':') < 0) {
const rawQuestion = await this.fetchRawQuestionById(id, params);
const orderBookEvent = this.parseEvent(rawQuestion);
this.indexEventOutcomes(orderBookEvent);
return orderBookEvent;
}
const response = await this.fetchRawMarketById(id, params);
const market = this.parseMyriadMarket(response);
const event = this.parseMarketToEvent(response, market);
this.indexEventOutcomes(event);
return event;
}
/**
* @ignore
* @method
* @name myriad#fetchRawMarketById
* @description fetches a single raw myriad market object by its unified event id (a composite networkId:marketId)
* @param {string} id the unified event/market id
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} the raw myriad market object
*/
async fetchRawMarketById(id, params = {}) {
// the unified event id is a composite networkId:marketId
const parts = id.split(':');
const partsLength = parts.length;
const request = {};
if (partsLength > 1) {
request['network_id'] = this.safeString(parts, 0);
request['id'] = this.safeString(parts, 1);
}
else {
request['id'] = id;
}
return await this.myriadPublicGetMarketsId(this.extend(request, params));
}
/**
* @ignore
* @method
* @name myriad#fetchRawQuestionById
* @description fetches a single raw myriad question object by question id; falls back to keyword search by id/slug/title when direct lookup is unavailable
* @param {string} id the question id or slug
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object} the raw question object
*/
async fetchRawQuestionById(id, params = {}) {
const request = {
'id': id,
};
let result = undefined;
try {
result = await this.myriadPublicGetQuestionsId(this.extend(request, params));
}
catch (e) {
if ((e instanceof RateLimitExceeded) || (e instanceof AuthenticationError)) {
throw e;
}
const keywordRequest = {
'keyword': id,
'limit': 50,
};
const response = await this.myriadPublicGetQuestions(this.extend(keywordRequest, params));
const questions = this.safeList(response, 'data', []);
const questionsLength = questions.length;
const idLower = id.toLowerCase();
for (let i = 0; i < questionsLength; i++) {
const q = this.safeDict(questions, i, {});
const qId = this.safeString(q, 'id', '');
const qSlug = this.safeString(q, 'slug', '');
const qTitle = this.safeString(q, 'title', '');
const qHandle = this.shortenSlug(qSlug);
if ((qId.toLowerCase() === idLower) || (qSlug.toLowerCase() === idLower) || (qTitle.toLowerCase() === idLower) || ((qHandle !== undefined) && (qHandle.toLowerCase() === idLower))) {
return q;
}
}
throw e;
}
return result;
}
/**
* @ignore
* @method
* @name myriad#fetchRawQuestionsBySearch
* @description fetches raw myriad question objects matching the given search terms via the questions keyword filter
* @param {string[]} queries search terms
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object[]} an array of raw myriad question objects
*/
async fetchRawQuestionsBySearch(queries, params = {}) {
const limit = this.safeInteger(params, 'limit', this.safeInteger(this.options, 'defaultFetchEventsLimit', 50));
const rest = this.omit(params, ['limit']);
const seen = {};
const rawQuestions = [];
for (let i = 0; i < queries.length; i++) {
const q = queries[i];
const response = await this.myriadPublicGetQuestions(this.extend({
'keyword': q,
'limit': limit,
}, rest));
const foundList = this.safeList(response, 'data', response);
const found = (foundList !== undefined) ? foundList : [];
for (let j = 0; j < found.length; j++) {
const raw = found[j];
const questionId = this.safeString(raw, 'id');
if ((questionId !== undefined) && !(questionId in seen)) {
seen[questionId] = true;
rawQuestions.push(raw);
}
}
}
return rawQuestions;
}
/**
* @ignore
* @method
* @name myriad#fetchRawQuestionsList
* @description fetches raw myriad question objects from the paginated questions listing
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @param {string} [params.state] optional question state filter when supported by the backend
* @returns {object[]} an array of raw myriad question objects
*/
async fetchRawQuestionsList(params = {}) {
const limit = this.safeInteger(this.options, 'defaultFetchEventsLimit', 50);
const maxQuestions = this.safeInteger(params, 'limit', this.safeInteger(this.options, 'fetchEventsLimit', 1000));
const state = this.safeString2(params, 'state', 'status', this.safeString(this.options, 'defaultMarketStatus', 'open'));
const rest = this.omit(params, ['state', 'status', 'limit', 'tradingModel', 'trading_model']);
const allRawQuestions = [];
const seen = {};
let collected = 0;
let page = 1;
while (true) {
const request = {
'limit': limit,
'page': page,
};
if (state !== undefined) {
request['state'] = state;
}
const response = await this.myriadPublicGetQuestions(this.extend(request, rest));
const rawQuestionsList = this.safeList(response, 'data', response);
const rawQuestions = (rawQuestionsList !== undefined) ? rawQuestionsList : [];
const rawQuestionsLength = rawQuestions.length;
if (rawQuestionsLength === 0) {
break;
}
for (let i = 0; i < rawQuestionsLength; i++) {
const rawQuestion = rawQuestions[i];
const questionId = this.safeString(rawQuestion, 'id');
if ((questionId !== undefined) && (questionId in seen)) {
continue;
}
if (questionId !== undefined) {
seen[questionId] = true;
}
if (collected < maxQuestions) {
allRawQuestions.push(rawQuestion);
collected = this.sum(collected, 1);
}
}
page = this.sum(page, 1);
if ((rawQuestionsLength < limit) || (collected >= maxQuestions)) {
break;
}
}
return allRawQuestions;
}
/**
* @method
* @name myriad#fetchPositions
* @description fetch the open outcome-token positions held by a wallet (myriad settles trades on-chain, so only read-only portfolio data is exposed by the API)
* @see https://docs.myriad.markets/builders/myriad-api-reference
* @param {string[]} [outcomes] unified outcomes to filter by
* @param {object} [params] extra exchange-specific parameters
* @param {string} [params.address] the wallet address to query, defaults to this.walletAddress
* @returns {object[]} a list of [prediction position structures](https://docs.ccxt.com/#/?id=prediction-position-structure)
*/
async fetchPositions(outcomes = undefined, params = {}) {
// resolve the owner the same way fetchBalance does — derive from the configured privateKey
// when no explicit walletAddress/param is set, so a privateKey-only config works for both
const address = this.safeString2(params, 'address', 'user', this.walletAddressOrUndefined());
if (address === undefined) {
throw new ArgumentsRequired(this.id + ' fetchPositions() requires a walletAddress or an address parameter');
}
const rest = this.omit(params, ['address', 'user']);
const response = await this.myriadPublicGetUsersAddressPortfolio(this.extend({ 'address': address }, rest));
//
// {
// "data": [
// {
// "marketId": 170145,
// "marketTitle": "Will Base TGE in 2026?",
// "marketSlug": "will-base-tge-in-2026",
// "imageUrl": "https://cdn.polkamarkets.com/Qmacfs1qiiUW5cnMRUyzji393Vn2DcvNdydGukf1Xk82b6",
// "outcomeId": 0,
// "outcomeTitle": "Yes",
// "networkId": 56,
// "token": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d",
// "tokenId": null,
// "shares": 8.23666644,
// "price": 0.1214083400468503,
// "value": 0.9823048396344001,
// "profit": -0.017695160365599896,
// "roi": -0.017695160365599896,
// "totalProfit": -0.017695160365599927,
// "totalRoi": -0.017695160365599927,
// "positionFees": 0.02,
// "totalFees": 0.02,
// "winningsToClaim": false,
// "winningsClaimed": false,
// "voidedWinningsToClaim": false,
// "voidedWinningsClaimed": false,
// "status": "ongoing",
// "claimed": false,
// "executionMode": 0,
// "expiresAt": "2026-12-31 23:59:00",
// "eventId": null
// }
// ],
// "pagination": {
// "page": 1,
// "limit": 20,
// "total": 1,
// "totalPages": 1,
// "hasNext": false,
// "hasPrev": false
// }
// }
//
const data = this.safeList(response, 'data', []);
const result = [];
for (let i = 0; i < data.length; i++) {
result.push(this.parsePredictionPosition(data[i]));
}
return this.filterByArray(result, 'outcome', outcomes, false);
}
/**
* @ignore
* @method
* @name myriad#parsePredictionPosition
* @description parses a raw myriad portfolio entry into a unified position structure
* @param {object} position the raw portfolio entry
* @param {object} [market] not used by myriad
* @returns {object} a [prediction position structure](https://docs.ccxt.com/#/?id=prediction-position-structure)
*/
parsePredictionPosition(position, market = undefined) {
const marketSlug = this.safeString(position, 'marketSlug', '');
const outcomeTitle = this.safeString(position, 'outcomeTitle', '');
const outcome = this.slugToOutcomeSymbol(marketSlug, marketSlug, outcomeTitle);
const marketSymbol = this.slugToMarketSymbol(marketSlug, marketSlug);
const networkId = this.safeString(position, 'networkId');
const marketId = this.safeString(position, 'marketId');
const outcomeId = this.safeString(position, 'outcomeId');
const id = networkId + ':' + marketId + '/' + outcomeId;
const shares = this.safeNumber(position, 'shares');
const value = this.safeNumber(position, 'value');
const profit = this.safeNumber(position, 'profit');
const roi = this.safeString(position, 'roi');
let percentage = undefined;
if (roi !== undefined) {
percentage = Precise.stringMul(roi, '100');
}
return this.safePredictionPosition({
'info': position,
'id': id,
'outcome': outcome,
'outcomeId': outcomeId,
'label': outcomeTitle,
'market': marketSymbol,
'contracts': shares,
'side': 'long',
'notional': value,
'markPrice': this.safeNumber(position, 'price'),
'unrealizedPnl': profit,
'percentage': this.parseNumber(percentage),
'marginMode': 'cash',
'hedged': false,
});
}
/**
* @method
* @name myriad#fetchTradeQuote
* @description fetches a trade quote — price, shares, fees and the on-chain calldata — for buying or selling an outcome. Myriad settles trades on-chain, so this returns the calldata to submit to the prediction-market contract rather than placing an off-chain order
* @see https://docs.myriad.markets/builders/myriad-api-reference
* @param {string} outcome unified outcome or outcome id
* @param {string} side 'buy' or 'sell'
* @param {float} amount for 'buy' the collateral value to spend; for 'sell' the number of shares to sell
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @param {float} [params.slippage] maximum slippage tolerance (default 0.005)
* @returns {object} a quote object with price, shares, fees and the on-chain calldata
*/
async fetchTradeQuote(outcome, side, amount, params = {}) {
await this.loadOutcome(outcome);
const outcomeObj = this.outcome(outcome);
const info = this.safeDict(outcomeObj, 'info', {});
const networkId = this.safeString(info, 'networkId');
const marketId = this.safeString(info, 'marketId');
const outcomeId = this.safeInteger(info, 'outcomeId');
const sideStr = side.toLowerCase();
const request = {
'market_id': this.parseToInt(marketId),
'network_id': this.parseToInt(networkId),
'outcome_id': outcomeId,
'action': sideStr,
'slippage': this.safeNumber(params, 'slippage', 0.005),
};
if (sideStr === 'buy') {
request['value'] = amount;
}
else {
request['shares'] = amount;
}
const rest = this.omit(params, ['slippage']);
const response = await this.myriadPublicPostMarketsQuote(this.extend(request, rest));
//
// {
// "value": 10,
// "shares": 21.566766528674936,
// "shares_threshold": 21.45893269603156,
// "price_average": 0.4636763692278168,
// "price_before": 0.46100295,
// "price_after": 0.46635187379825593,
// "calldata": "0x1...680",
// "net_amount": 10,
// "fees": {
// "treasury": 0,
// "distributor": 0,
// "fee": 0
// }
// }
//
return this.parseTradeQuote(this.extend(response, { 'action': sideStr }), outcomeObj);
}
/**
* @ignore
* @method
* @name myriad#parseTradeQuote
* @description parses a raw myriad quote response into a unified-ish quote object
* @param {object} quote the raw quote response
* @param {object} [market] the outcome the quote belongs to
* @returns {object} a quote object
*/
parseTradeQuote(quote, market = undefined) {
//
// {
// "value": 10,
// "shares": 21.566766528674936,
// "shares_threshold": 21.45893269603156,
// "price_average": 0.4636763692278168,
// "price_before": 0.46100295,
// "price_after": 0.46635187379825593,
// "calldata": "0x1...680",
// "net_amount": 10,
// "fees": {
// "treasury": 0,
// "distributor": 0,
// "fee": 0
// }
// }
//
return {
'outcome': this.safeString(market, 'outcome'),
'side': this.safeStringLower(quote, 'action'),
'value': this.safeNumber(quote, 'value'),
'shares': this.safeNumber(quote, 'shares'),
'sharesThreshold': this.safeNumber(quote, 'shares_threshold'),
'priceAverage': this.safeNumber(quote, 'price_average'),
'priceBefore': this.safeNumber(quote, 'price_before'),
'priceAfter': this.safeNumber(quote, 'price_after'),
'netAmount': this.safeNumber(quote, 'net_amount'),
'fees': this.safeDict(quote, 'fees'),
'calldata': this.safeString(quote, 'calldata'),
'info': quote,
};
}
signEvmTransaction(tx, privateKey) {
// builds and signs an EIP-1559 (type 0x02) transaction, returning the signed raw tx hex.
// tx fields (nonce/gas/fees/value) are hex strings; chainId is an int. Verified
// byte-identical to ethers' serialization
const accessList = this.rlpEncodeList([]);
const fields = [
this.rlpEncodeBytes(this.intToRlpHex(this.safeInteger(tx, 'chainId'))),
this.rlpEncodeBytes(this.hexToRlpBytes(this.safeString(tx, 'nonce'))),
this.rlpEncodeBytes(this.hexToRlpBytes(this.safeString(tx, 'maxPriorityFeePerGas'))),
this.rlpEncodeBytes(this.hexToRlpBytes(this.safeString(tx, 'maxFeePerGas'))),
this.rlpEncodeBytes(this.hexToRlpBytes(this.safeString(tx, 'gasLimit'))),
this.rlpEncodeBytes(this.remove0xPrefix(this.safeString(tx, 'to'))),
this.rlpEncodeBytes(this.hexToRlpBytes(this.safeString(tx, 'value', '0x0'))),
this.rlpEncodeBytes(this.remove0xPrefix(this.safeString(tx, 'data', '0x'))),
accessList,
];
const payload = '02' + this.rlpEncodeList(fields);
const hashHex = this.hash(this.base16ToBinary(payload), keccak, 'hex');
const signature = ecdsa(hashHex, this.remove0xPrefix(privateKey), secp256k1, undefined);
let rHex = this.safeString(signature, 'r');
let sHex = this.safeString(signature, 's');
if (rHex === undefined) {
throw new ExchangeError(this.id + ' signEvmTransaction() missing rHex');
}
if ((rHex.length % 2) !== 0) {
rHex = '0' + rHex;
}
if (sHex === undefined) {
throw new ExchangeError(this.id + ' signEvmTransaction() missing sHex');
}
if ((sHex.length % 2) !== 0) {
sHex = '0' + sHex;
}
const yParity = this.safeInteger(signature, 'v');
const signedFields = [];
for (let i = 0; i < fields.length; i++) {
signedFields.push(fields[i]);
}
signedFields.push(this.rlpEncodeBytes(this.intToRlpHex(yParity)));
signedFields.push(this.rlpEncodeBytes(rHex));
signedFields.push(this.rlpEncodeBytes(sHex));
return '0x02' + this.rlpEncodeList(signedFields);
}
async ethRpc(rpcUrl, method, rpcParams) {
const payload = { 'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': rpcParams };
const headers = { 'Content-Type': 'application/json' };
const response = await this.fetch(rpcUrl, 'POST', headers, this.json(payload));
const rpcError = this.safeValue(response, 'error');
if (rpcError !== undefined) {
throw new ExchangeError(this.id + ' rpc ' + method + ' error: ' + this.json(rpcError));
}
// the result is either a hex string (nonce/gasPrice/txhash) or an object (receipt) —
// safeString would coerce a receipt object to "[object Object]"
return this.safeValue(response, 'result');
}
async ensureErc20Allowance(rpcUrl, networkId, token, owner, spender) {
// allowance(owner, spender)
const allowanceData = '0xdd62ed3e' + this.padHexAddress(owner) + this.padHexAddress(spender);
const current = await this.ethRpc(rpcUrl, 'eth_call', [{ 'to': token, 'data': allowanceData }, 'latest']);
const trimmed = this.hexToRlpBytes(current);
// a max-approved allowance is ~32 bytes (64 nibbles); anything much smaller needs (re)approval
if (trimmed.length >= 50) {
return undefined;
}
// approve(spender, maxUint256)
const maxUint = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
const approveData = '0x095ea7b3' + this.padHexAddress(spender) + maxUint;
const approveHash = await this.sendEvmTransaction(rpcUrl, this.parseToInt(networkId), owner, token, '0x0', approveData, '0x186a0');
await this.waitForTransactionReceipt(rpcUrl, approveHash);
return undefined;
}
/**
* @method
* @name myriad#createOrder
* @description create a trade order. Myriad has two trading models: a gasless order book (CLOB) where an EIP-712 signed order is posted off-chain and settled by the operator, and an on-chain AMM. Order-book markets are used by default; the model can be forced via params.tradingModel
* @see https://docs.myriad.markets/builders/myriad-order-book/order-book-api#37dc9e49da8281e2bc49cf4914b07528
* @param {string} outcome unified outcome or outcome id
* @param {string} type 'limit' or 'market' (order book); ignored by the AMM path
* @param {string} side 'buy' or 'sell'
* @param {float} amount number of outcome shares to trade (AMM 'buy' spends this as collateral value instead)
* @param {float} [price] price per share as a fraction in [0, 1] (required for order-book limit orders)
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @param {string} [params.tradingModel] 'ob' to force the order book, 'amm' to force the on-chain AMM; defaults to the market's model
* @param {string} [params.timeInForce] order-book time in force: 'GTC', 'GTD', 'FOK', 'FAK' or 'PO'
* @param {string} [params.expiration] unix-seconds expiration for a GTD order
* @returns {object} a [prediction order structure](https://docs.ccxt.com/#/?id=prediction-order-structure)
*/
async createOrder(outcome, type, side, amount, price = undefined, params = {}) {
const outcomeObj = await this.loadOutcome(outcome);
const info = this.safeDict(outcomeObj, 'info', {});
const defaultModel = this.safeString(info, 'tradingModel', 'amm');
const tradingModel = this.safeStringLower(params, 'tradingModel', defaultModel);
const rest = this.omit(params, ['tradingModel']);
if (tradingModel === 'ob') {
return await this.createOrderbookOrder(outcome, type, side, amount, price, rest);
}
// the on-chain AMM path requires native gas and has not been verified end to end; keep it behind
// an explicit opt-in so callers do not silently hit an untested signing/broadcast path
const enableAmm = this.safeBool2(params, 'enableAmm', 'enableAmmOrders', this.safeBool(this.options, 'enableAmmOrders', false));
if (!enableAmm) {
throw new NotSupported(this.id + ' createOrder() only supports the gasless order book; this market uses the on-chain AMM (needs native gas and is unverified) — pass params.enableAmm=true to opt in');
}
return await this.createAmmOrder(outcome, type, side, amount, price, this.omit(rest, ['enableAmm', 'enableAmmOrders']));
}
/**
* @ignore
* @method
* @name myriad#createOrderbookOrder
* @description signs an EIP-712 order and posts it to the gasless order book; the operator settles the match on-chain
* @returns {object} a [prediction order structure](https://docs.ccxt.com/#/?id=prediction-order-structure)
*/
async createOrderbookOrder(outcome, type, side, amount, price = undefined, params = {}) {
const built = this.buildOrderbookOrder(outcome, type, side, amount, price, params);
const order = this.safeDict(built, 'order');
const networkId = this.safeString(built, 'networkId');
const timeInForce = this.safeString(built, 'timeInForce');
const request = {
'order': order,
'signature': this.safeString(built, 'signature'),
'network_id': this.parseToInt(networkId),
'time_in_force': timeInForce,
};
const response = await this.myriadPublicPostOrders(request);
//
// {
// "orderHash": "0x758a1763c59bbe61c314f3c0c9b5bae0ad942120500eb39e3e8349bbe13990e0",
// "status": "open",
// "timeInForce": "GTC"
// }
//
const orderForResponse = {
'trader': this.safeString(order, 'trader'),
'marketId': this.safeString(order, 'marketId'),
'outcomeId': this.safeNumber(order, 'outcomeId'),
'side': this.safeNumber(order, 'side'),
'amount': this.safeString(order, 'amount'),
'price': this.safeString(order, 'price'),
'minFillAmount': this.safeString(order, 'minFillAmount'),
'nonce': this.safeString(order, 'nonce'),
'expiration': this.safeString(order, 'expiration'),
};
const wrapper = this.extend(response, { 'order': orderForResponse, 'networkId': networkId, 'timeInForce': timeInForce });
const outcomeObj = this.outcome(outcome);
const parsed = this.parsePredictionOrder(wrapper, outcomeObj);
// the POST /orders response is minimal (hash + status), so backfill the known request values
// side/type/price/amount/timeInForce and a creation timestamp - when parsePredictionOrder left them empty
const sideStr = (side === undefined) ? undefined : side.toLowerCase();
const typeStr = (type === undefined) ? 'limit' : type.toLowerCase();
if (this.safeString(parsed, 'side') === undefined) {
parsed['side'] = sideStr;
}
if (this.safeString(parsed, 'type') === undefined) {
parsed['type'] = typeStr;
}
if (this.safeString(parsed, 'timeInForce') === undefined) {
parsed['timeInForce'] = timeInForce;
}
if ((this.safeNumber(parsed, 'price') === undefined) && (price !== undefined)) {
parsed['price'] = price;
}
if ((this.safeNumber(parsed, 'amount') === undefined) && (amount !== undefined)) {
parsed['amount'] = amount;
}
if (this.safeInteger(parsed, 'timestamp') === undefined) {
const now = this.milliseconds();
parsed['timestamp'] = now;
parsed['datetime'] = this.iso8601(now);
}
if (this.safeString(parsed, 'status') === undefined) {
parsed['status'] = 'open';
}
return parsed;
}
/**
* @ignore
* @method
* @name myriad#buildOrderbookOrder
* @description builds and EIP-712 signs a single order-book order; shared by createOrder and createOrders
* @returns {object} a dict with the signed order, signature, timeInForce and networkId
*/
buildOrderbookOrder(outcome, type, side, amount, price = undefined, params = {}) {
if (this.privateKey === undefined) {
throw new ArgumentsRequired(this.id + ' createOrder() requires a privateKey to sign the order');
}
const outcomeObj = this.outcome(outcome);
const info = this.safeDict(outcomeObj, 'info', {});
const networkId = this.safeString(info, 'networkId', this.safeString(this.options, 'defaultNetworkId', '56'));
const marketId = this.safeString(info, 'marketId');
const outcomeId = this.safeInteger(info, 'outcomeId', 0);
const trader = this.ethGetAddressFromPrivateKey(this.privateKey);
const typeStr = (type === undefined) ? 'limit' : type.toLowerCase();
const sideStr = side.toLowerCase();
const sideInt = (sideStr === 'buy') ? 0 : 1;
const isMarket = (typeStr === 'market');
const defaultTif = isMarket ? 'FOK' : 'GTC';
const timeInForce = this.safeStringUpper(params, 'timeInForce', defaultTif);
let priceValue = price;
if (priceValue === undefined) {
if (isMarket) {
priceValue = (sideInt === 0) ? 1 : 0;
}
else {
throw new ArgumentsRequired(this.id + ' createOrder() requires a price for limit orders');
}
}
let priceWei = this.toOrderbookWei(priceValue);
if (Precise.stringLt(priceWei, '1')) {
priceWei = '1';
}
// price is a fraction in (0, 1] encoded as 1..1e18 wei (tick is 1 wei); reject out-of-range early
if (Precise.stringGt(priceWei, '1000000000000000000')) {
throw new InvalidOrder(this.id + ' createOrder() price must be a fraction between 0 and 1');
}
const amountWei = this.toOrderbookWei(amount);
// shares are integer wei (1e18 = 1 share); a sub-wei amount that rounds to zero is invalid
if (Precise.stringLt(amountWei, '1')) {
throw new InvalidOrder(this.id + ' createOrder() amount is too small (rounds to zero shares)');
}
const nonce = this.safeString(params, 'nonce', this.numberToString(this.milliseconds()));
const expiration = this.safeString(params, 'expiration', '0');
const minFillAmount = this.safeString(params, 'minFillAmount', '0');
const order = {
'trader': trader,
'marketId': marketId,
'outcomeId': this.parseToNumeric(outcomeId),
'side': this.parseToNumeric(sideInt),
'amount': amountWei,
'price': priceWei,
'minFillAmount': minFillAmount,
'nonce': nonce,
'expiration': expiration,
};
const signature = this.signClobOrder(order, networkId);
return {
'order': order,
'signature': signature,
'timeInForce': timeInForce,
'networkId': networkId,
};
}
/**
* @method
* @name myriad#createOrders
* @description places multiple order book orders. Myriad's batch endpoint is not reliable, so the
* orders are signed and submitted sequentially (not atomically)
* @see https://docs.myriad.markets/builders/myriad-order-book/order-book-api#37dc9e49da8281e2bc49cf4914b07528
* @param {object[]} orders a list of order requests, each with outcome, type, side, amount, price and params
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @returns {object[]} a list of [prediction order structures](https://docs.ccxt.com/#/?id=prediction-order-structure)
*/
async createOrders(orders, params = {}) {
const ordersLength = orders.length;
const orderOutcomes = [];
for (let i = 0; i < ordersLength; i++) {
const __oc = this.safeString(orders[i], 'outcome');
if (__oc !== undefined) {
orderOutcomes.push(__oc);
}
}
await this.loadOutcomes(orderOutcomes);
const result = [];
for (let i = 0; i < ordersLength; i++) {
const o = orders[i];
const outcome = this.safeString(o, 'outcome');
const type = this.safeString(o, 'type');
const side = this.safeString(o, 'side');
const amount = this.safeNumber(o, 'amount');
const price = this.safeNumber(o, 'price');
const orderParams = this.safeDict(o, 'params', {});
const placed = await this.createOrderbookOrder(outcome, type, side, amount, price, this.extend(orderParams, params));
result.push(placed);
}
return result;
}
/**
* @method
* @name myriad#editOrder
* @description edits an open order by cancelling it and placing a replacement (gasless). Myriad's
* batch-modify endpoint is not reliable, so the cancel and replace are submitted sequentially
* @see https://docs.myriad.markets/builders/myriad-order-book/order-book-api#37dc9e49da8281b58c5adb2f5998eec8
* @param {string} id the hash of the order to replace
* @param {string} outcome unified outcome of the new order
* @param {string} type 'limit' or 'market'
* @param {string} side 'buy' or 'sell'
* @param {float} amount number of outcome shares for the new order
* @param {float} [price] price per share as a fraction in [0, 1]
* @param {object} [params] extra parameters specific to the exchange API endpoint
* @param {object} [params.orderResponse] a pre-fetched fetchOrder-style response fo