raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
918 lines • 63.1 kB
JavaScript
"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