UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

918 lines 63.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Raiden = void 0; require("./polyfills"); const bignumber_1 = require("@ethersproject/bignumber"); const constants_1 = require("@ethersproject/constants"); const providers_1 = require("@ethersproject/providers"); const isUndefined_1 = __importDefault(require("lodash/isUndefined")); const memoize_1 = __importDefault(require("lodash/memoize")); const omitBy_1 = __importDefault(require("lodash/omitBy")); const loglevel_1 = __importDefault(require("loglevel")); const redux_1 = require("redux"); const developmentOnly_1 = require("redux-devtools-extension/developmentOnly"); const redux_logger_1 = require("redux-logger"); const redux_observable_1 = require("redux-observable"); const rxjs_1 = require("rxjs"); const fetch_1 = require("rxjs/fetch"); const operators_1 = require("rxjs/operators"); const actions_1 = require("./actions"); const actions_2 = require("./channels/actions"); const state_1 = require("./channels/state"); const utils_1 = require("./channels/utils"); const config_1 = require("./config"); const constants_2 = require("./constants"); const contracts_1 = require("./contracts"); const utils_2 = require("./db/utils"); const epics_1 = require("./epics"); const helpers_1 = require("./helpers"); const persister_1 = require("./persister"); const reducer_1 = require("./reducer"); const actions_3 = require("./services/actions"); const types_1 = require("./services/types"); const utils_3 = require("./services/utils"); const actions_4 = require("./transfers/actions"); const state_2 = require("./transfers/state"); const utils_4 = require("./transfers/utils"); const actions_5 = require("./transport/actions"); const types_2 = require("./types"); const utils_5 = require("./utils"); const actions_6 = require("./utils/actions"); const data_1 = require("./utils/data"); const error_1 = require("./utils/error"); const ethers_1 = require("./utils/ethers"); const rx_1 = require("./utils/rx"); const types_3 = require("./utils/types"); const versions_json_1 = __importDefault(require("./versions.json")); class Raiden { /** * Constructs a Raiden instance from state machine parameters * * It expects ready Redux and Epics params, with some async members already resolved and set in * place, therefore this constructor is expected to be used only for tests and advancecd usage * where finer control is needed to tweak how some of these members are initialized; * Most users should usually prefer the [[create]] async factory, which already takes care of * these async initialization steps and accepts more common parameters. * * @param state - Validated and decoded initial/rehydrated RaidenState * @param deps - Constructed epics dependencies object, including signer, provider, fetched * network and contracts information. * @param epic - State machine root epic * @param reducer - State machine root reducer */ constructor(state, deps, epic = (0, epics_1.combineRaidenEpics)(), reducer = reducer_1.raidenReducer) { this.deps = deps; this.epic = epic; /** * action$ exposes the internal events pipeline. It's intended for debugging, and its interface * must not be relied on, as its actions interfaces and structures can change without warning. */ this.action$ = this.deps.latest$.pipe((0, rx_1.pluckDistinct)('action'), (0, operators_1.skip)(1)); /** * state$ is exposed only so user can listen to state changes and persist them somewhere else. * Format/content of the emitted objects are subject to changes and not part of the public API */ this.state$ = this.deps.latest$.pipe((0, rx_1.pluckDistinct)('state')); /** * channels$ is public interface, exposing a view of the currently known channels * Its format is expected to be kept backwards-compatible, and may be relied on */ this.channels$ = this.state$.pipe((0, rx_1.pluckDistinct)('channels'), (0, operators_1.map)(helpers_1.mapRaidenChannels)); /** * A subset ot RaidenActions exposed as public events. * The interface of the objects emitted by this Observable are expected not to change internally, * but more/new events may be added over time. */ this.events$ = this.action$.pipe((0, operators_1.filter)((0, actions_6.isActionOf)(actions_1.RaidenEvents))); /** * Observable of completed and pending transfers * Every time a transfer state is updated, it's emitted here. 'key' property is unique and * may be used as identifier to know which transfer got updated. */ this.transfers$ = (0, helpers_1.initTransfers$)(this.deps.db); /** RaidenConfig observable (for reactive use) */ this.config$ = this.deps.config$; /** Observable of latest average (10) block times */ this.blockTime$ = this.deps.latest$.pipe((0, rx_1.pluckDistinct)('blockTime')); /** When started, is set to a promise which resolves when node finishes syncing */ this.synced = (0, helpers_1.makeSyncedPromise)(this.action$); /** * Get constant token details from token contract, caches it. * Rejects only if 'token' contract doesn't define totalSupply and decimals methods. * name and symbol may be undefined, as they aren't actually part of ERC20 standard, although * very common and defined on most token contracts. * * @param token - address to fetch info from * @returns token info */ this.getTokenInfo = (0, helpers_1.makeTokenInfoGetter)(this.deps); /** Expose ether's Provider.resolveName for ENS support */ this.resolveName = this.deps.provider.resolveName.bind(this.deps.provider); /** The address of the token that is used to pay the services (SVT/RDN). */ this.userDepositTokenAddress = (0, memoize_1.default)(async () => this.deps.userDepositContract.callStatic.token()); // use next from latest known blockNumber as start block when polling deps.provider.resetEventsBlock(state.blockNumber + 1); const isBrowser = !!globalThis?.location; const loggerMiddleware = (0, redux_logger_1.createLogger)({ predicate: () => this.log.getLevel() <= loglevel_1.default.levels.INFO, logger: this.log, level: { prevState: false, action: 'info', error: 'error', nextState: 'debug', }, ...(isBrowser ? {} : { colors: false }), }); // minimum blockNumber of contracts deployment as start scan block this.epicMiddleware = (0, redux_observable_1.createEpicMiddleware)({ dependencies: deps }); const persisterMiddleware = (0, persister_1.createPersisterMiddleware)(deps.db); this.store = (0, redux_1.createStore)(reducer, // workaround for redux's PreloadedState issues with branded values state, // eslint-disable-line @typescript-eslint/no-explicit-any (0, developmentOnly_1.composeWithDevTools)((0, redux_1.applyMiddleware)(loggerMiddleware, persisterMiddleware, this.epicMiddleware))); deps.config$.subscribe((config) => (this.config = config)); // populate deps.latest$, to ensure config, logger && pollingInterval are setup before start (0, epics_1.getLatest$)((0, rxjs_1.of)((0, actions_1.raidenConfigUpdate)({})), (0, rxjs_1.of)(this.store.getState()), deps).subscribe((latest) => deps.latest$.next(latest)); } /** * Async helper factory to make a Raiden instance from more common parameters. * * An async factory is needed so we can do the needed async requests to construct the required * parameters ahead of construction time, and avoid partial initialization then * * @param this - Raiden class or subclass * @param connection - A URL or provider to connect to, one of: * <ul> * <li>JsonRpcProvider instance,</li> * <li>a Metamask's web3.currentProvider object or,</li> * <li>a hostname or remote json-rpc connection string</li> * </ul> * @param account - An account to use as main account, one of: * <ul> * <li>Signer instance (e.g. Wallet) loaded with account/private key or</li> * <li>hex-encoded string address of a remote account in provider or</li> * <li>hex-encoded string local private key or</li> * <li>number index of a remote account loaded in provider * (e.g. 0 for Metamask's loaded account)</li> * </ul> * @param storage - diverse storage related parameters to load from and save to * @param storage.state - State uploaded by user; should be decodable by RaidenState; * it is auto-migrated * @param storage.adapter - PouchDB adapter; default to 'indexeddb' on browsers and 'leveldb' on * node. If you provide a custom one, ensure you call PouchDB.plugin on it. * @param storage.prefix - Database name prefix; use to set a directory to store leveldown db; * @param contractsOrUDCAddress - Contracts deployment info, or UserDeposit contract address * @param config - Raiden configuration * @param subkey - Whether to use a derived subkey or not * @param subkeyOriginUrl - URL of origin to generate a subkey for (defaults * to global context) * @returns Promise to Raiden SDK client instance */ static async create(connection, account, // eslint-disable-next-line @typescript-eslint/no-explicit-any storage, contractsOrUDCAddress, config, subkey, subkeyOriginUrl) { let provider; if (typeof connection === 'string') { provider = new providers_1.JsonRpcProvider(connection); } else if (connection instanceof providers_1.JsonRpcProvider) { provider = connection; } else { provider = new providers_1.Web3Provider(connection); } const network = await provider.getNetwork(); const { signer, address, main } = await (0, helpers_1.getSigner)(account, provider, subkey, subkeyOriginUrl); // Build initial state or parse from database const { state, db } = await (0, helpers_1.getState)({ provider, network, address, log: loglevel_1.default.getLogger(`raiden:${address}`) }, contractsOrUDCAddress, storage); const contractsInfo = state.contracts; (0, utils_5.assert)(address === state.address, [ error_1.ErrorCodes.RDN_STATE_ADDRESS_MISMATCH, { account: address, state: state.address, }, ]); (0, utils_5.assert)(network.chainId === state.chainId, [ error_1.ErrorCodes.RDN_STATE_NETWORK_MISMATCH, { network: network.chainId, contracts: contractsInfo, stateNetwork: state.chainId, }, ]); const cleanConfig = config && (0, types_3.decode)(config_1.PartialRaidenConfig, (0, omitBy_1.default)(config, isUndefined_1.default)); const deps = (0, helpers_1.makeDependencies)(state, cleanConfig, { signer, contractsInfo, db, main }); return new this(state, deps); } /** * Starts redux/observables by subscribing to all epics and emitting initial state and action * * No event should be emitted before start is called */ async start() { (0, utils_5.assert)(this.epicMiddleware, error_1.ErrorCodes.RDN_ALREADY_STARTED, this.log.info); this.log.info('Starting Raiden Light-Client', { prevBlockNumber: this.state.blockNumber, address: this.address, contracts: this.deps.contractsInfo, network: this.deps.network, 'raiden-ts': Raiden.version, 'raiden-contracts': Raiden.contractVersion, config: this.config, versions: process?.versions, }); // Set `epicMiddleware` to `null`, this indicates the instance is not running. this.deps.latest$.subscribe({ complete: () => (this.epicMiddleware = null), }); this.epicMiddleware.run(this.epic); // prevent start from being called again, turns this.started to true this.epicMiddleware = undefined; // dispatch a first, noop action, to next first state$ as current/initial state this.store.dispatch((0, actions_1.raidenStarted)()); await this.synced; } /** * Gets the running state of the instance * * @returns undefined if not yet started, true if running, false if already stopped */ get started() { // !epicMiddleware -> undefined | null -> undefined ? true/started : null/stopped; if (!this.epicMiddleware) return this.epicMiddleware === undefined; // else -> !!epicMiddleware -> not yet started -> returns undefined } /** * Triggers all epics to be unsubscribed */ async stop() { // start still can't be called again, but turns this.started to false // this.epicMiddleware is set to null by latest$'s complete callback if (this.started) this.store.dispatch((0, actions_1.raidenShutdown)({ reason: constants_2.ShutdownReason.STOP })); if (this.started !== undefined) await (0, rxjs_1.lastValueFrom)(this.deps.db.busy$, { defaultValue: undefined }); } /** * Instance's Logger, compatible with console's API * * @returns Logger object */ get log() { return this.deps.log; } /** * Get current RaidenState object. Can be serialized safely with [[encodeRaidenState]] * * @returns Current Raiden state */ get state() { return this.store.getState(); } /** * Current provider getter * * @returns ether's provider instance */ get provider() { return this.deps.provider; } /** * Get current account address (subkey's address, if subkey is being used) * * @returns Instance address */ get address() { return this.deps.address; } /** * Get main account address (if subkey is being used, undefined otherwise) * * @returns Main account address */ get mainAddress() { return this.deps.main?.address; } /** * Get current network from provider * * @returns Network object containing blockchain's name & chainId */ get network() { return this.deps.network; } /** * Returns a promise to current block number, as seen in provider and state * * @returns Promise to current block number */ async getBlockNumber() { const lastBlockNumber = this.deps.provider.blockNumber; if (lastBlockNumber && lastBlockNumber >= this.deps.contractsInfo.TokenNetworkRegistry.block_number) return lastBlockNumber; else return await this.deps.provider.getBlockNumber(); } /** * Returns the currently used SDK version. * * @returns SDK version */ static get version() { return versions_json_1.default.sdk; } /** * Returns the version of the used Smart Contracts. * * @returns Smart Contract version */ static get contractVersion() { return versions_json_1.default.contracts; } /** * Returns the Smart Contracts addresses and deployment blocks * * @returns Smart Contracts info */ get contractsInfo() { return this.deps.contractsInfo; } /** * Update Raiden Config with a partial (shallow) object * * @param config - Partial object containing keys and values to update in config */ updateConfig(config) { if ('mediationFees' in config) // just validate, it's set in getLatest$ this.deps.mediationFeeCalculator.decodeConfig(config.mediationFees, this.deps.defaultConfig.mediationFees); this.store.dispatch((0, actions_1.raidenConfigUpdate)((0, types_3.decode)(config_1.PartialRaidenConfig, config))); } /** * Dumps database content for backup * * @yields Rows of objects */ async *dumpDatabase() { yield* (0, utils_2.dumpDatabase)(this.deps.db); } /** * Get ETH balance for given address or self * * @param address - Optional target address. If omitted, gets own balance * @returns BigNumber of ETH balance */ getBalance(address) { address = address ?? (0, helpers_1.chooseOnchainAccount)(this.deps, this.config.subkey).address; (0, utils_5.assert)(types_3.Address.is(address), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { address }], this.log.info); return this.deps.provider.getBalance(address); } /** * Get token balance and token decimals for given address or self * * @param token - Token address to fetch balance. Must be one of the monitored tokens. * @param address - Optional target address. If omitted, gets own balance * @returns BigNumber containing address's token balance */ async getTokenBalance(token, address) { address = address ?? (0, helpers_1.chooseOnchainAccount)(this.deps, this.config.subkey).address; (0, utils_5.assert)(types_3.Address.is(address), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { address }], this.log.info); (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); const tokenContract = this.deps.getTokenContract(token); return tokenContract.callStatic.balanceOf(address); } /** * Returns a list of all token addresses registered as token networks in registry * * @param rescan - Whether to rescan events from scratch * @returns Promise to list of token addresses */ async getTokenList(rescan = false) { if (!rescan) return Object.keys(this.state.tokens); return await (0, rxjs_1.lastValueFrom)((0, ethers_1.getLogsByChunk$)(this.deps.provider, { ...this.deps.registryContract.filters.TokenNetworkCreated(), fromBlock: this.deps.contractsInfo.TokenNetworkRegistry.block_number, toBlock: await this.getBlockNumber(), }).pipe((0, operators_1.map)((log) => this.deps.registryContract.interface.parseLog(log)), (0, operators_1.filter)((parsed) => !!parsed.args['token_address']), (0, operators_1.map)((parsed) => parsed.args['token_address']), (0, operators_1.toArray)())); } /** * Scans initially and start monitoring a token for channels with us, returning its Tokennetwork * address * * Throws an exception if token isn't registered in current registry * * @param token - token address to monitor, must be registered in current token network registry * @returns Address of TokenNetwork contract */ async monitorToken(token) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); let tokenNetwork = this.state.tokens[token]; if (tokenNetwork) return tokenNetwork; tokenNetwork = (await this.deps.registryContract.token_to_token_networks(token)); (0, utils_5.assert)(tokenNetwork && tokenNetwork !== constants_1.AddressZero, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); this.store.dispatch((0, actions_2.tokenMonitored)({ token, tokenNetwork, fromBlock: this.deps.contractsInfo.TokenNetworkRegistry.block_number, })); return tokenNetwork; } /** * Open a channel on the tokenNetwork for given token address with partner * * If token isn't yet monitored, starts monitoring it * * @param token - Token address on currently configured token network registry * @param partner - Partner address * @param options - (optional) option parameter * @param options.deposit - Deposit to perform in parallel with channel opening * @param options.confirmConfirmation - Whether to wait `confirmationBlocks` after last * transaction confirmation; default=true if confirmationBlocks * @param onChange - Optional callback for status change notification * @returns txHash of channelOpen call, iff it succeeded */ async openChannel(token, partner, options = {}, onChange) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(partner), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info); const tokenNetwork = await this.monitorToken(token); const deposit = !options.deposit ? undefined : (0, types_3.decode)((0, types_3.UInt)(32), options.deposit, error_1.ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info); const confirmConfirmation = options.confirmConfirmation ?? !!this.config.confirmationBlocks; const meta = { tokenNetwork, partner }; // wait for confirmation const openPromise = (0, actions_6.asyncActionToPromise)(actions_2.channelOpen, meta, this.action$).then(({ txHash }) => { onChange?.({ type: types_2.EventTypes.OPENED, payload: { txHash } }); return txHash; // pluck txHash }); const openConfirmedPromise = (0, actions_6.asyncActionToPromise)(actions_2.channelOpen, meta, this.action$, true).then(({ txHash }) => { onChange?.({ type: types_2.EventTypes.CONFIRMED, payload: { txHash } }); return txHash; // pluck txHash }); let depositPromise; if (deposit?.gt(0)) { depositPromise = (0, actions_6.asyncActionToPromise)(actions_2.channelDeposit, meta, this.action$.pipe( // ensure we only react on own deposit's responses (0, operators_1.filter)((action) => !actions_2.channelDeposit.success.is(action) || action.payload.participant === this.address)), true).then(({ txHash }) => { onChange?.({ type: types_2.EventTypes.DEPOSITED, payload: { txHash } }); return txHash; // pluck txHash }); } this.store.dispatch(actions_2.channelOpen.request({ ...options, deposit }, meta)); const [, openTxHash, depositTxHash] = await Promise.all([ openPromise, openConfirmedPromise, depositPromise, ]); if (confirmConfirmation) { // wait twice confirmationBlocks for deposit or open tx await (0, helpers_1.waitConfirmation)(await this.deps.provider.getTransactionReceipt(depositTxHash ?? openTxHash), this.deps, this.config.confirmationBlocks * 2); } return openTxHash; } /** * Deposit tokens on channel between us and partner on tokenNetwork for token * * @param token - Token address on currently configured token network registry * @param partner - Partner address * @param amount - Number of tokens to deposit on channel * @param options - tx options * @param options.confirmConfirmation - Whether to wait `confirmationBlocks` after last * transaction confirmation; default=true if config.confirmationBlocks * @returns txHash of setTotalDeposit call, iff it succeeded */ async depositChannel(token, partner, amount, { confirmConfirmation = !!this.config.confirmationBlocks, } = {}) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(partner), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info); const state = this.state; const tokenNetwork = state.tokens[token]; (0, utils_5.assert)(tokenNetwork, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); const deposit = (0, types_3.decode)((0, types_3.UInt)(32), amount, error_1.ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info); const meta = { tokenNetwork, partner }; const promise = (0, actions_6.asyncActionToPromise)(actions_2.channelDeposit, meta, this.action$.pipe( // ensure we only react on own deposit's responses (0, operators_1.filter)((action) => !actions_2.channelDeposit.success.is(action) || action.payload.participant === this.address)), true).then(({ txHash }) => txHash); this.store.dispatch(actions_2.channelDeposit.request({ deposit }, meta)); const depositTxHash = await promise; if (confirmConfirmation) { // wait twice confirmationBlocks for deposit or open tx await (0, helpers_1.waitConfirmation)(await this.deps.provider.getTransactionReceipt(depositTxHash), this.deps, this.config.confirmationBlocks * 2); } return depositTxHash; } /** * Close channel between us and partner on tokenNetwork for token * This method will fail if called on a channel not in 'opened' or 'closing' state. * When calling this method on an 'opened' channel, its state becomes 'closing', and from there * on, no payments can be performed on the channel. If for any reason the closeChannel * transaction fails, channel's state stays as 'closing', and this method can be called again * to retry sending 'closeChannel' transaction. After it's successful, channel becomes 'closed', * and can be settled after 'settleTimeout' seconds (when it then becomes 'settleable'). * * @param token - Token address on currently configured token network registry * @param partner - Partner address * @returns txHash of closeChannel call, iff it succeeded */ async closeChannel(token, partner) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(partner), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info); const state = this.state; const tokenNetwork = state.tokens[token]; (0, utils_5.assert)(tokenNetwork, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); // try coop-settle first try { const channel = this.state.channels[(0, utils_1.channelKey)({ tokenNetwork, partner })]; (0, utils_5.assert)(channel, 'channel not found'); const { ownTotalWithdrawable: totalWithdraw } = (0, utils_1.channelAmounts)(channel); const expiration = Math.ceil(Date.now() / 1e3 + this.config.revealTimeout * this.config.expiryFactor); const coopMeta = { direction: state_2.Direction.SENT, tokenNetwork, partner, totalWithdraw, expiration, }; const coopPromise = (0, actions_6.asyncActionToPromise)(actions_4.withdraw, coopMeta, this.action$, true).then(({ txHash }) => txHash); this.store.dispatch((0, actions_4.withdrawResolve)({ coopSettle: true }, coopMeta)); return await coopPromise; } catch (err) { this.log.info('Could not settle cooperatively, performing uncooperative close', err); } const meta = { tokenNetwork, partner }; const promise = (0, actions_6.asyncActionToPromise)(actions_2.channelClose, meta, this.action$, true).then(({ txHash }) => txHash); this.store.dispatch(actions_2.channelClose.request(undefined, meta)); return promise; } /** * Settle channel between us and partner on tokenNetwork for token * This method will fail if called on a channel not in 'settleable' or 'settling' state. * Channel becomes 'settleable' settleTimeout seconds after closed (detected automatically * while Raiden Light Client is running or later on restart). When calling it, channel state * becomes 'settling'. If for any reason transaction fails, it'll stay on this state, and this * method can be called again to re-send a settleChannel transaction. * * @param token - Token address on currently configured token network registry * @param partner - Partner address * @returns txHash of settleChannel call, iff it succeeded */ async settleChannel(token, partner) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(partner), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info); const state = this.state; const tokenNetwork = state.tokens[token]; (0, utils_5.assert)(tokenNetwork, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); (0, utils_5.assert)(!this.config.autoSettle, error_1.ErrorCodes.CNL_SETTLE_AUTO_ENABLED, this.log.info); const meta = { tokenNetwork, partner }; // wait for channel to become settleable await (0, rxjs_1.lastValueFrom)((0, helpers_1.waitChannelSettleable$)(this.state$, meta)); // wait for the corresponding success or error action const promise = (0, actions_6.asyncActionToPromise)(actions_2.channelSettle, meta, this.action$, true).then(({ txHash }) => txHash); this.store.dispatch(actions_2.channelSettle.request(undefined, meta)); return promise; } /** * Returns object describing address's users availability on transport * After calling this method, any further presence update to valid transport peers of this * address will trigger a corresponding MatrixPresenceUpdateAction on events$ * * @param address - checksummed address to be monitored * @returns Promise to object describing availability and last event timestamp */ async getAvailability(address) { (0, utils_5.assert)(types_3.Address.is(address), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { address }], this.log.info); const meta = { address }; const promise = (0, actions_6.asyncActionToPromise)(actions_5.matrixPresence, meta, this.action$); this.store.dispatch(actions_5.matrixPresence.request(undefined, meta)); return promise; } /** * Get list of past and pending transfers * * @param filter - Filter options * @param filter.pending - true: only pending; false: only completed; undefined: all * @param filter.token - filter by token address * @param filter.partner - filter by partner address * @param filter.end - filter by initiator or target address * @param options - PouchDB.ChangesOptions object * @param options.offset - Offset to skip entries * @param options.limit - Limit number of entries * @param options.desc - Set to true to get newer transfers first * @returns promise to array of all transfers */ async getTransfers(filter, options) { return (0, utils_2.getTransfers)(this.deps.db, filter, options); } /** * Send a Locked Transfer! * This will reject if LockedTransfer signature prompt is canceled/signature fails, or be * resolved to the transfer unique identifier (secrethash) otherwise, and transfer status can be * queried with this id on this.transfers$ observable, which will just have emitted the 'pending' * transfer. Any following transfer state change will be notified through this observable. * * @param token - Token address on currently configured token network registry * @param target - Target address * @param value - Amount to try to transfer * @param options - Optional parameters for transfer: * @param options.paymentId - payment identifier, a random one will be generated if missing * @param options.secret - Secret to register, a random one will be generated if missing * @param options.secrethash - Must match secret, if both provided, or else, secret must be * informed to target by other means, and reveal can't be performed * @param options.paths - Used to specify possible routes & fees instead of querying PFS. * Should receive a decodable super-set of the public RaidenPaths interface * @param options.pfs - Use this PFS instead of configured or automatically choosen ones. * Is ignored if paths were already provided. If neither are set and config.pfs is not * disabled (null), use it if set or if undefined (auto mode), fetches the best * PFS from ServiceRegistry and automatically fetch routes from it. * @param options.lockTimeout - Specify a lock timeout for transfer; * default is expiryFactor * revealTimeout * @param options.encryptSecret - Whether to force encrypting the secret or not, * if target supports it * @returns A promise to transfer's unique key (id) when it's accepted */ async transfer(token, target, value, options = {}) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(target), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { target }], this.log.info); const tokenNetwork = this.state.tokens[token]; (0, utils_5.assert)(tokenNetwork, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); const decodedValue = (0, types_3.decode)((0, types_3.UInt)(32), value, error_1.ErrorCodes.DTA_INVALID_AMOUNT, this.log.info); const paymentId = options.paymentId !== undefined ? (0, types_3.decode)((0, types_3.UInt)(8), options.paymentId, error_1.ErrorCodes.DTA_INVALID_PAYMENT_ID, this.log.info) : (0, utils_4.makePaymentId)(); const paths = options.paths && (0, types_3.decode)(types_1.InputPaths, options.paths, error_1.ErrorCodes.DTA_INVALID_PATH, this.log.info); const pfs = options.pfs && (0, types_3.decode)(types_1.PFS, options.pfs, error_1.ErrorCodes.DTA_INVALID_PFS, this.log.info); (0, utils_5.assert)(options.secret === undefined || types_3.Secret.is(options.secret), error_1.ErrorCodes.RDN_INVALID_SECRET, this.log.info); (0, utils_5.assert)(options.secrethash === undefined || types_3.Hash.is(options.secrethash), error_1.ErrorCodes.RDN_INVALID_SECRETHASH, this.log.info); // use provided secret or create one if no secrethash was provided const secret = options.secret || (options.secrethash ? undefined : (0, utils_4.makeSecret)()); const secrethash = options.secrethash || (0, utils_4.getSecrethash)(secret); (0, utils_5.assert)(!secret || (0, utils_4.getSecrethash)(secret) === secrethash, error_1.ErrorCodes.RDN_SECRET_SECRETHASH_MISMATCH, this.log.info); const promise = (0, rxjs_1.firstValueFrom)(this.action$.pipe((0, operators_1.filter)((0, actions_6.isActionOf)([actions_4.transferSigned, actions_4.transfer.failure])), (0, operators_1.filter)(({ meta }) => meta.direction === state_2.Direction.SENT && meta.secrethash === secrethash), (0, operators_1.map)((action) => { if (actions_4.transfer.failure.is(action)) throw action.payload; return (0, utils_4.transferKey)(action.meta); }))); this.store.dispatch(actions_4.transfer.request({ tokenNetwork, target, value: decodedValue, paymentId, secret, resolved: false, paths, pfs, lockTimeout: options.lockTimeout, encryptSecret: options.encryptSecret, }, { secrethash, direction: state_2.Direction.SENT })); return promise; } /** * Waits for the transfer identified by a secrethash to fail or complete * The returned promise will resolve with the final transfer state, or reject if anything fails * * @param transferKey - Transfer identifier as returned by [[transfer]] * @returns Promise to final RaidenTransfer */ async waitTransfer(transferKey) { const { direction, secrethash } = (0, utils_4.transferKeyToMeta)(transferKey); let transferState = this.state.transfers[transferKey]; if (!transferState) try { transferState = (0, types_3.decode)(state_2.TransferState, await this.deps.db.get(transferKey)); } catch (e) { } (0, utils_5.assert)(transferState, error_1.ErrorCodes.RDN_UNKNOWN_TRANSFER, this.log.info); const raidenTransf = (0, utils_4.raidenTransfer)(transferState); // already completed/past transfer if (raidenTransf.completed) { if (raidenTransf.success) return raidenTransf; else throw new error_1.RaidenError(error_1.ErrorCodes.XFER_ALREADY_COMPLETED, { status: raidenTransf.status }); } // throws/rejects if a failure occurs await (0, actions_6.asyncActionToPromise)(actions_4.transfer, { secrethash, direction }, this.action$); const finalState = await (0, rxjs_1.firstValueFrom)(this.state$.pipe((0, operators_1.pluck)('transfers', transferKey), (0, operators_1.filter)((transferState) => !!transferState.unlockProcessed))); this.log.info('Transfer successful', { key: transferKey, partner: finalState.partner, initiator: finalState.transfer.initiator, target: finalState.transfer.target, fee: finalState.fee.toString(), lockAmount: finalState.transfer.lock.amount.toString(), targetReceived: finalState.secretRequest?.amount.toString(), transferTime: finalState.unlockProcessed.ts - finalState.transfer.ts, }); return (0, utils_4.raidenTransfer)(finalState); } /** * Request a path from PFS * * If a direct route is possible, it'll be returned. Else if PFS is set up, a request will be * performed and the cleaned/validated path results will be resolved. * Else, if no route can be found, promise is rejected with respective error. * * @param token - Token address on currently configured token network registry * @param target - Target address * @param value - Minimum capacity required on routes * @param options - Optional parameters * @param options.pfs - Use this PFS instead of configured or automatically choosen ones * @returns A promise to returned routes/paths result */ async findRoutes(token, target, value, options = {}) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(target), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { target }], this.log.info); const tokenNetwork = this.state.tokens[token]; (0, utils_5.assert)(tokenNetwork, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); const decodedValue = (0, types_3.decode)((0, types_3.UInt)(32), value); const pfs = options.pfs ? (0, types_3.decode)(types_1.PFS, options.pfs) : undefined; const meta = { tokenNetwork, target, value: decodedValue }; const promise = (0, actions_6.asyncActionToPromise)(actions_3.pathFind, meta, this.action$).then(({ paths }) => paths); this.store.dispatch(actions_3.pathFind.request({ pfs }, meta)); return promise; } /** * Checks if a direct transfer of token to target could be performed and returns it on a * single-element array of Paths * * @param token - Token address on currently configured token network registry * @param target - Target address * @param value - Minimum capacity required on route * @returns Promise to a [Raiden]Paths array containing the single, direct route, or undefined */ async directRoute(token, target, value) { (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); (0, utils_5.assert)(types_3.Address.is(target), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { target }], this.log.info); const tokenNetwork = this.state.tokens[token]; (0, utils_5.assert)(tokenNetwork, error_1.ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info); const decodedValue = (0, types_3.decode)((0, types_3.UInt)(32), value); const meta = { tokenNetwork, target, value: decodedValue }; const promise = (0, actions_6.asyncActionToPromise)(actions_3.pathFind, meta, this.action$).then(({ paths }) => paths, // pluck paths () => undefined); // dispatch a pathFind with pfs disabled, to force checking for a direct route this.store.dispatch(actions_3.pathFind.request({ pfs: null }, meta)); return promise; } /** * Returns a sorted array of info of available PFS * * It uses data polled from ServiceRegistry, which is available only when config.pfs is * undefined, instead of set or disabled (null), and will reject if not. * It can reject if the validated list is empty, meaning we can be out-of-sync (we're outdated or * they are) with PFSs deployment, or no PFS is available on this TokenNetwork/blockchain. * * @returns Promise to array of PFS, which is the interface which describes a PFS */ async findPFS() { (0, utils_5.assert)(this.config.pfsMode !== types_1.PfsMode.disabled, error_1.ErrorCodes.PFS_DISABLED, this.log.info); await this.synced; const services = [...this.config.additionalServices]; if (this.config.pfsMode === types_1.PfsMode.auto) services.push(...Object.keys(this.state.services)); return (0, rxjs_1.lastValueFrom)((0, utils_3.pfsListInfo)(services, this.deps)); } /** * Mints the amount of tokens of the provided token address. * Throws an error, if * <ol> * <li>Executed on main net</li> * <li>`token` or `options.to` is not a valid address</li> * <li>Token could not be minted</li> * </ol> * * @param token - Address of the token to be minted * @param amount - Amount to be minted * @param options - tx options * @param options.to - Beneficiary, defaults to mainAddress or address * @returns transaction */ async mint(token, amount, { to } = {}) { // Check whether address is valid (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); // Check whether we are on a test network (0, utils_5.assert)(this.deps.network.chainId !== 1, error_1.ErrorCodes.RDN_MINT_MAINNET, this.log.info); const { signer, address } = (0, helpers_1.chooseOnchainAccount)(this.deps, this.config.subkey); // Mint token const customTokenContract = contracts_1.CustomToken__factory.connect(token, signer); const beneficiary = to ?? address; (0, utils_5.assert)(types_3.Address.is(beneficiary), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { beneficiary }], this.log.info); const value = (0, types_3.decode)((0, types_3.UInt)(32), amount, error_1.ErrorCodes.DTA_INVALID_AMOUNT); const [, receipt] = await (0, rxjs_1.lastValueFrom)((0, utils_1.transact)(customTokenContract, 'mintFor', [value, beneficiary], this.deps, { error: error_1.ErrorCodes.RDN_MINT_FAILED, })); // wait for a single block, so future calls will correctly pick value await (0, helpers_1.waitConfirmation)(receipt, this.deps, 1); return receipt.transactionHash; } /** * Registers and creates a new token network for the provided token address. * Throws an error, if * <ol> * <li>Executed on main net</li> * <li>`token` is not a valid address</li> * <li>Token is already registered</li> * <li>Token could not be registered</li> * </ol> * * @param token - Address of the token to be registered * @param channelParticipantDepositLimit - The deposit limit per channel participant * @param tokenNetworkDepositLimit - The deposit limit of the whole token network * @returns Address of new token network */ async registerToken(token, channelParticipantDepositLimit = constants_1.MaxUint256, tokenNetworkDepositLimit = constants_1.MaxUint256) { // Check whether address is valid (0, utils_5.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info); // Check whether we are on a test network (0, utils_5.assert)(this.deps.network.chainId !== 1, error_1.ErrorCodes.RDN_REGISTER_TOKEN_MAINNET, this.log.info); const { signer } = (0, helpers_1.chooseOnchainAccount)(this.deps, this.config.subkey); const tokenNetworkRegistry = (0, helpers_1.getContractWithSigner)(this.deps.registryContract, signer); // Check whether token is already registered await this.monitorToken(token).then((tokenNetwork) => { throw new error_1.RaidenError(error_1.ErrorCodes.RDN_REGISTER_TOKEN_REGISTERED, { tokenNetwork }); }, () => undefined); const [, receipt] = await (0, rxjs_1.lastValueFrom)((0, utils_1.transact)(tokenNetworkRegistry, 'createERC20TokenNetwork', [token, channelParticipantDepositLimit, tokenNetworkDepositLimit], this.deps, { error: error_1.ErrorCodes.RDN_REGISTER_TOKEN_FAILED })); await (0, helpers_1.waitConfirmation)(receipt, this.deps); return await this.monitorToken(token); } /** * Fetches balance of UserDeposit Contract for SDK's account minus cached spent IOUs * * @returns Promise to UDC remaining capacity */ async getUDCCapacity() { const balance = await (0, helpers_1.getUdcBalance)(this.deps.latest$); const now = Math.round(Date.now() / 1e3); // in seconds const owedAmount = Object.values(this.state.iou) .reduce((acc, value) => [ ...acc, ...Object.values(value).filter((value) => value.claimable_until.gte(now)), ], []) .reduce((acc, iou) => acc.add(iou.amount), constants_1.Zero); return balance.sub(owedAmount); } /** * Fetches total_deposit of UserDeposit Contract for SDK's account * * The usable part of the deposit should be fetched with [[getUDCCapacity]], but this function * is useful when trying to deposit based on the absolute value of totalDeposit. * * @returns Promise to UDC total deposit */ async getUDCTotalDeposit() { return (0, rxjs_1.firstValueFrom)(this.deps.latest$.pipe((0, operators_1.pluck)('udcDeposit', 'totalDeposit'), (0, operators_1.filter)((deposit) => !!deposit && deposit.lt(constants_1.MaxUint256)))); } /** * Deposits the amount to the UserDeposit contract with the target/signer as a beneficiary. * The deposited amount can be used as a collateral in order to sign valid IOUs that will * be accepted by the Services. * * Throws an error, in the following cases: * <ol> * <li>The amount specified equals to zero</li> * <li>The target has an insufficient token balance</li> * <li>The "approve" transaction fails with an error</li> * <li>The "deposit" transaction fails with an error</li> * </ol> * * @param amount - The amount to deposit on behalf of the target/beneficiary. * @param onChange - callback providing notifications about state changes * @returns transaction hash */ async depositToUDC(amount, onChange) { const deposit = (0, types_3.decode)((0, types_3.UInt)(32), amount, error_1.ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info); (0, utils_5.assert)(deposit.gt(constants_1.Zero), error_1.ErrorCodes.DTA_NON_POSITIVE_NUMBER, this.log.info); const deposited = await this.deps.userDepositContract.callStatic.total_deposit(this.address); const meta = { totalDeposit: deposited.add(deposit) }; const mined = (0, actions_6.asyncActionToPromise)(actions_3.udcDeposit, meta, this.action$, false).then(({ txHash }) => onChange?.({ type: types_2.EventTypes.DEPOSITED, payload: { txHash } })); this.store.dispatch(actions_3.udcDeposit.request({ deposit }, meta)); const confirmed = (0, actions_6.asyncActionToPromise)(actions_3.udcDeposit, meta, this.action$, true).then(({ txHash }) => { onChange?.({ type: types_2.EventTypes.CONFIRMED, payload: { txHash } }); return txHash; }); const [, txHash] = await Promise.all([mined, confirmed]); return txHash; } /** * Transfer value ETH on-chain to address. * If subkey is being used, use main account by default, or subkey account if 'subkey' is true * Example: * // transfer 0.1 ETH from main account to subkey account, when subkey is used * await raiden.transferOnchainBalance(raiden.address, parseEther('0.1')); * // transfer entire balance from subkey account back to main account * await raiden.transferOnchainBalance(raiden.mainAddress, undefined, { subkey: true }); * * @param to - Recipient address * @param value - Amount of ETH (in Wei) to transfer. Use ethers/utils::parseEther if needed * Defaults to a very big number, which will cause all entire balance to be transfered * @param options - tx options * @param options.subkey - By default, if using subkey, main account is used t