raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
317 lines • 14.8 kB
JavaScript
;
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