UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

317 lines 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.choosePfs$ = exports.signIOU = exports.packIOU = exports.getPresenceFromService$ = exports.validateAddressMetadata = exports.pfsListInfo = exports.pfsInfoAddress = exports.pfsInfo = void 0; const bytes_1 = require("@ethersproject/bytes"); const hash_1 = require("@ethersproject/hash"); const signing_key_1 = require("@ethersproject/signing-key"); const transactions_1 = require("@ethersproject/transactions"); const t = __importStar(require("io-ts")); const constant_1 = __importDefault(require("lodash/constant")); const memoize_1 = __importDefault(require("lodash/memoize")); const uniqBy_1 = __importDefault(require("lodash/uniqBy")); const rxjs_1 = require("rxjs"); const fetch_1 = require("rxjs/fetch"); const operators_1 = require("rxjs/operators"); const utils_1 = require("../messages/utils"); const actions_1 = require("../transport/actions"); const utils_2 = require("../transport/utils"); const data_1 = require("../utils/data"); const error_1 = require("../utils/error"); const lru_1 = require("../utils/lru"); const rx_1 = require("../utils/rx"); const types_1 = require("../utils/types"); const types_2 = require("./types"); const serviceRegistryToken = (0, memoize_1.default)(async (serviceRegistryContract, pollingInterval) => (0, rxjs_1.firstValueFrom)((0, rx_1.retryAsync$)(async () => serviceRegistryContract.callStatic.token(), pollingInterval, { onErrors: error_1.networkErrors }))); /** * Fetch, validate and cache the service URL for a given URL or service address * (if registered on ServiceRegistry) * * @param pfsAddressUrl - service Address or URL * @returns Promise to validated URL */ const pfsAddressUrl = (0, memoize_1.default)(async function pfsAddressUrl_(pfsAddrOrUrl, { serviceRegistryContract }) { let url = pfsAddrOrUrl; if (types_1.Address.is(pfsAddrOrUrl)) url = await serviceRegistryContract.callStatic.urls(pfsAddrOrUrl); return validatePfsUrl(url); }); const urlRegex = process.env['NODE_ENV'] === 'production' ? /^(?:https:\/\/)?[^\s\/$.?#&"']+\.[^\s\/$?#&"']+$/ : /^(?:(http|https):\/\/)?([^\s\/$.?#&"']+\.)*[^\s\/$?#&"']+(?:(\d+))*$/; function validatePfsUrl(url) { (0, error_1.assert)(url, error_1.ErrorCodes.PFS_EMPTY_URL); (0, error_1.assert)(urlRegex.test(url), [error_1.ErrorCodes.PFS_INVALID_URL, { url }]); // default to https for schema-less urls if (!url.match(/^https?:\/\//)) url = `https://${url}`; return url; } const pfsAddressCache_ = new lru_1.LruCache(32); /** * Fetch PFS info & validate for a given server address or URL * * This is a memoized function which caches by url or address, network and registry used. * * @param pfsAddrOrUrl - PFS account/address or URL * @param deps - RaidenEpicDeps needed for various parameters * @param deps.log - Logger instance * @param deps.serviceRegistryContract - ServiceRegistry contract instance * @param deps.network - Current Network * @param deps.contractsInfo - ContractsInfo mapping * @param deps.provider - Eth provider * @param deps.config$ - Config observable * @param deps.latest$ - Latest observable * @returns Promise containing PFS server info */ async function pfsInfo(pfsAddrOrUrl, { log, serviceRegistryContract, network, contractsInfo, provider, config$, latest$, }) { const { pfsMaxFee } = await (0, rxjs_1.firstValueFrom)(config$); const { state } = await (0, rxjs_1.firstValueFrom)(latest$); const { services } = state; /** Codec for PFS /api/v1/info result schema */ const PfsInfo = t.type({ matrix_server: t.string, network_info: t.type({ // literals will fail if trying to decode anything different from these constants chain_id: t.literal(network.chainId), token_network_registry_address: t.literal(contractsInfo.TokenNetworkRegistry.address), }, 'NetworkInfo'), payment_address: types_1.Address, price_info: (0, types_1.UInt)(32), }, 'PfsInfo'); try { // if it's an address, fetch url from ServiceRegistry, else it's already the URL const url = await pfsAddressUrl(pfsAddrOrUrl, { serviceRegistryContract }); const start = Date.now(); const res = await fetch(url + '/api/v1/info', { mode: 'cors' }); const rtt = Date.now() - start; const text = await res.text(); (0, error_1.assert)(res.ok, [error_1.ErrorCodes.PFS_ERROR_RESPONSE, { text }]); const info = (0, types_1.decode)(PfsInfo, (0, data_1.jsonParse)(text)); const { payment_address: address, price_info: price } = info; (0, error_1.assert)(price.lte(pfsMaxFee), [error_1.ErrorCodes.PFS_TOO_EXPENSIVE, { price }]); pfsAddressCache_.set(url, Promise.resolve(address)); const validTill = services[address] ?? (await serviceRegistryContract.callStatic.service_valid_till(address)).toNumber() * 1e3; return { address, url, matrixServer: info.matrix_server, rtt, price, token: await serviceRegistryToken(serviceRegistryContract, provider.pollingInterval), validTill, }; } catch (err) { log.warn('Error fetching PFS info:', pfsAddrOrUrl, err); throw err; } } exports.pfsInfo = pfsInfo; /** * Returns the address for the PFS/service with the given URL. * Result is cached and this cache is shared with [[pfsInfo]] calls. * * @param url - Url of the PFS to retrieve address for * @param deps - Epics dependencies (for pfsInfo) * @returns Promise to Address of PFS on given URL */ exports.pfsInfoAddress = Object.assign(async function pfsInfoAddress(url, deps) { url = validatePfsUrl(url); let addrPromise = pfsAddressCache_.get(url); if (!addrPromise) { // since the url is already validated, this will always set the cache, even if to the promise // which will be rejected on '/info' request/validation addrPromise = pfsInfo(url, deps).then(({ address }) => address); pfsAddressCache_.set(url, addrPromise); } return addrPromise; }, { cache: pfsAddressCache_ }); /** * Retrieve pfsInfo for these servers & return sorted PFS info * * Sort order is price then response time (rtt). * Throws if no server can be validated, meaning either there's none in the current network or * we're out-of-sync (outdated or ahead of PFS's deployment network version). * * @param pfsList - Array of PFS addresses or URLs * @param deps - RaidenEpicDeps array * @returns Observable of online, validated & sorted PFS info array */ function pfsListInfo(pfsList, deps) { return (0, rxjs_1.from)(pfsList).pipe((0, operators_1.mergeMap)((addrOrUrl) => (0, rxjs_1.defer)(async () => pfsInfo(addrOrUrl, deps)).pipe((0, operators_1.catchError)((0, constant_1.default)(rxjs_1.EMPTY))), 5), (0, operators_1.toArray)(), (0, operators_1.map)((list) => { (0, error_1.assert)(list.length || !pfsList.length, error_1.ErrorCodes.PFS_INVALID_INFO); return (0, uniqBy_1.default)(list, 'url').sort((a, b) => { const dif = a.price.sub(b.price); // first, sort by price if (dif.lt(0)) return -1; else if (dif.gt(0)) return 1; // if it's equal, tiebreak on rtt else return a.rtt - b.rtt; }); })); } exports.pfsListInfo = pfsListInfo; /** * @param metadata - to convert to presence * @returns presence for metadata, assuming node is available */ function metadataToPresence(metadata) { const pubkey = (0, signing_key_1.recoverPublicKey)((0, bytes_1.arrayify)((0, hash_1.hashMessage)(metadata.user_id)), metadata.displayname); const address = (0, transactions_1.computeAddress)(pubkey); return actions_1.matrixPresence.success({ userId: metadata.user_id, available: true, ts: Date.now(), caps: (0, utils_2.parseCaps)(metadata.capabilities), pubkey, }, { address }); } /** * Validates metadata was signed by address * * @param metadata - Peer's metadata * @param address - Peer's address * @param opts - Options * @param opts.log - Logger instance * @returns presence iff metadata is valid and was signed by address */ function validateAddressMetadata(metadata, address, { log } = {}) { if (!metadata) return; try { const presence = metadataToPresence(metadata); (0, error_1.assert)(presence.meta.address === address, [ 'Wrong signature', { expected: address, recovered: presence.meta.address }, ]); return presence; } catch (error) { log?.warn('Invalid address metadata', { address, metadata, error }); } } exports.validateAddressMetadata = validateAddressMetadata; /** * @param address - Peer address to fetch presence for * @param pfsAddrOrUrl - PFS/service address to fetch presence from * @param deps - Epics dependencies subset * @param deps.serviceRegistryContract - Contract instance * @returns Observable to peer's presence or error */ function getPresenceFromService$(address, pfsAddrOrUrl, deps) { return (0, rxjs_1.defer)(async () => pfsAddressUrl(pfsAddrOrUrl, deps)).pipe((0, rx_1.withMergeFrom)((url) => (0, fetch_1.fromFetch)(`${url}/api/v1/address/${address}/metadata`).pipe((0, operators_1.mergeMap)(async (res) => res.json()))), (0, operators_1.map)(([url, json]) => { try { const metadata = (0, types_1.decode)(types_2.AddressMetadata, json); const presence = validateAddressMetadata(metadata, address, deps); (0, error_1.assert)(presence, ['Invalid metadata signature', { peer: address, presence: metadata }]); (0, error_1.assert)(presence.payload.caps, ['Invalid capabilities format', metadata.capabilities]); return presence; } catch (err) { try { const { errors: msg, ...details } = (0, types_1.decode)(types_2.PfsError, json); err = new error_1.RaidenError(msg, { ...details, pfs: url }); } catch (e) { } throw err; } })); } exports.getPresenceFromService$ = getPresenceFromService$; /** * Pack an IOU for signing or verification * * @param iou - IOU to be packed * @returns Packed IOU as a UInt8Array */ function packIOU(iou) { return (0, bytes_1.concat)([ (0, data_1.encode)(iou.one_to_n_address, 20), (0, data_1.encode)(iou.chain_id, 32), (0, data_1.encode)(utils_1.MessageTypeId.IOU, 32), (0, data_1.encode)(iou.sender, 20), (0, data_1.encode)(iou.receiver, 20), (0, data_1.encode)(iou.amount, 32), (0, data_1.encode)(iou.claimable_until, 32), ]); } exports.packIOU = packIOU; /** * Sign an IOU with signer * * @param signer - Signer instance * @param iou - IOU to be signed * @returns Signed IOU */ async function signIOU(signer, iou) { return signer .signMessage(packIOU(iou)) .then((signature) => ({ ...iou, signature: signature })); } exports.signIOU = signIOU; /** * Choose best PFS and fetch info from it * * @param pfsByAction - Override config for this call: explicit PFS, disabled or undefined * @param deps - Epics dependencies * @param sortByRtt - Whether to sort PFSs by rtt, instead of price (default) * @returns Observable to choosen PFS */ function choosePfs$(pfsByAction, deps, sortByRtt = false) { const { log, config$, latest$, init$ } = deps; return config$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(({ pfsMode, additionalServices }) => { if (pfsByAction) return (0, rxjs_1.of)(pfsByAction); (0, error_1.assert)(pfsByAction !== null && pfsMode !== types_2.PfsMode.disabled, error_1.ErrorCodes.PFS_DISABLED); if (pfsMode === types_2.PfsMode.onlyAdditional) { let firstError; return (0, rxjs_1.from)(additionalServices).pipe((0, operators_1.concatMap)((service) => (0, rxjs_1.defer)(async () => pfsInfo(service, deps)).pipe((0, operators_1.catchError)((e) => ((firstError ?? (firstError = e)), rxjs_1.EMPTY)))), (0, operators_1.throwIfEmpty)(() => firstError)); } else { return latest$.pipe((0, rx_1.pluckDistinct)('state', 'services'), (0, operators_1.map)((services) => [...additionalServices, ...Object.keys(services)]), (0, operators_1.takeUntil)(init$.pipe((0, operators_1.last)(), (0, operators_1.delay)(10))), (0, operators_1.first)((services) => services.length > 0), // fetch pfsInfo from whole list & sort it (0, operators_1.mergeMap)((services) => { if (sortByRtt) return (0, rxjs_1.from)(services).pipe((0, operators_1.mergeMap)((addrOrUrl) => (0, rxjs_1.defer)(async () => pfsInfo(addrOrUrl, deps)).pipe((0, operators_1.catchError)((0, constant_1.default)(rxjs_1.EMPTY))))); else return pfsListInfo(services, deps).pipe((0, operators_1.mergeAll)()); // sort by price })); } }), (0, operators_1.throwIfEmpty)((0, constant_1.default)(new error_1.RaidenError(error_1.ErrorCodes.PFS_INVALID_INFO))), (0, operators_1.tap)((pfs) => { if (pfs.validTill < Date.now()) { log.warn('WARNING: PFS registration not valid! This service deposit may have expired and it may not receive network updates anymore.', pfs); } })); } exports.choosePfs$ = choosePfs$; //# sourceMappingURL=utils.js.map