UNPKG

ccxt

Version:

A cryptocurrency trading API with more than 100 exchanges in JavaScript / TypeScript / Python / C# / PHP / Go

1,016 lines (1,014 loc) 193 kB
// ---------------------------------------------------------------------------- // 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