raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
963 lines • 59.1 kB
JavaScript
import './polyfills';
import { BigNumber } from '@ethersproject/bignumber';
import { AddressZero, MaxUint256, Zero } from '@ethersproject/constants';
import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers';
import isUndefined from 'lodash/isUndefined';
import memoize from 'lodash/memoize';
import omitBy from 'lodash/omitBy';
import logging from 'loglevel';
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly';
import { createLogger } from 'redux-logger';
import { createEpicMiddleware } from 'redux-observable';
import { EMPTY, firstValueFrom, from, lastValueFrom, of } from 'rxjs';
import { fromFetch } from 'rxjs/fetch';
import { catchError, concatMap, filter, first, map, mergeMap, pluck, skip, timeout, toArray, } from 'rxjs/operators';
import { raidenConfigUpdate, RaidenEvents, raidenShutdown, raidenStarted } from './actions';
import { channelClose, channelDeposit, channelOpen, channelSettle, tokenMonitored, } from './channels/actions';
import { ChannelState } from './channels/state';
import { channelAmounts, channelKey, transact } from './channels/utils';
import { intervalFromConfig, PartialRaidenConfig } from './config';
import { ShutdownReason } from './constants';
import { CustomToken__factory } from './contracts';
import { dumpDatabase, getTransfers } from './db/utils';
import { combineRaidenEpics, getLatest$ } from './epics';
import { chooseOnchainAccount, getContractWithSigner, getSigner, getState, getUdcBalance, initTransfers$, makeDependencies, makeSyncedPromise, makeTokenInfoGetter, mapRaidenChannels, waitChannelSettleable$, waitConfirmation, } from './helpers';
import { createPersisterMiddleware } from './persister';
import { raidenReducer } from './reducer';
import { pathFind, udcDeposit, udcWithdraw, udcWithdrawPlan } from './services/actions';
import { InputPaths, PFS, PfsMode, SuggestedPartners } from './services/types';
import { pfsListInfo } from './services/utils';
import { transfer, transferSigned, withdraw, withdrawResolve } from './transfers/actions';
import { Direction, TransferState } from './transfers/state';
import { getSecrethash, makePaymentId, makeSecret, raidenTransfer, transferKey, transferKeyToMeta, } from './transfers/utils';
import { matrixPresence } from './transport/actions';
import { EventTypes } from './types';
import { assert } from './utils';
import { asyncActionToPromise, isActionOf } from './utils/actions';
import { jsonParse } from './utils/data';
import { commonTxErrors, ErrorCodes, RaidenError } from './utils/error';
import { getLogsByChunk$ } from './utils/ethers';
import { pluckDistinct, retryWhile } from './utils/rx';
import { Address, decode, Hash, Secret, UInt } from './utils/types';
import versions from './versions.json';
export 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 = combineRaidenEpics(), reducer = 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(pluckDistinct('action'), 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(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(pluckDistinct('channels'), map(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(filter(isActionOf(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$ = 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(pluckDistinct('blockTime'));
/** When started, is set to a promise which resolves when node finishes syncing */
this.synced = 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 = 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 = memoize(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 = createLogger({
predicate: () => this.log.getLevel() <= logging.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 = createEpicMiddleware({ dependencies: deps });
const persisterMiddleware = createPersisterMiddleware(deps.db);
this.store = createStore(reducer,
// workaround for redux's PreloadedState issues with branded values
state, // eslint-disable-line @typescript-eslint/no-explicit-any
composeWithDevTools(applyMiddleware(loggerMiddleware, persisterMiddleware, this.epicMiddleware)));
deps.config$.subscribe((config) => (this.config = config));
// populate deps.latest$, to ensure config, logger && pollingInterval are setup before start
getLatest$(of(raidenConfigUpdate({})), 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 JsonRpcProvider(connection);
}
else if (connection instanceof JsonRpcProvider) {
provider = connection;
}
else {
provider = new Web3Provider(connection);
}
const network = await provider.getNetwork();
const { signer, address, main } = await getSigner(account, provider, subkey, subkeyOriginUrl);
// Build initial state or parse from database
const { state, db } = await getState({ provider, network, address, log: logging.getLogger(`raiden:${address}`) }, contractsOrUDCAddress, storage);
const contractsInfo = state.contracts;
assert(address === state.address, [
ErrorCodes.RDN_STATE_ADDRESS_MISMATCH,
{
account: address,
state: state.address,
},
]);
assert(network.chainId === state.chainId, [
ErrorCodes.RDN_STATE_NETWORK_MISMATCH,
{
network: network.chainId,
contracts: contractsInfo,
stateNetwork: state.chainId,
},
]);
const cleanConfig = config && decode(PartialRaidenConfig, omitBy(config, isUndefined));
const deps = 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() {
assert(this.epicMiddleware, 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(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(raidenShutdown({ reason: ShutdownReason.STOP }));
if (this.started !== undefined)
await 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.sdk;
}
/**
* Returns the version of the used Smart Contracts.
*
* @returns Smart Contract version
*/
static get contractVersion() {
return versions.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(raidenConfigUpdate(decode(PartialRaidenConfig, config)));
}
/**
* Dumps database content for backup
*
* @yields Rows of objects
*/
async *dumpDatabase() {
yield* 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 ?? chooseOnchainAccount(this.deps, this.config.subkey).address;
assert(Address.is(address), [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 ?? chooseOnchainAccount(this.deps, this.config.subkey).address;
assert(Address.is(address), [ErrorCodes.DTA_INVALID_ADDRESS, { address }], this.log.info);
assert(Address.is(token), [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 lastValueFrom(getLogsByChunk$(this.deps.provider, {
...this.deps.registryContract.filters.TokenNetworkCreated(),
fromBlock: this.deps.contractsInfo.TokenNetworkRegistry.block_number,
toBlock: await this.getBlockNumber(),
}).pipe(map((log) => this.deps.registryContract.interface.parseLog(log)), filter((parsed) => !!parsed.args['token_address']), map((parsed) => parsed.args['token_address']), 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) {
assert(Address.is(token), [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));
assert(tokenNetwork && tokenNetwork !== AddressZero, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
this.store.dispatch(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) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(partner), [ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info);
const tokenNetwork = await this.monitorToken(token);
const deposit = !options.deposit
? undefined
: decode(UInt(32), options.deposit, ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info);
const confirmConfirmation = options.confirmConfirmation ?? !!this.config.confirmationBlocks;
const meta = { tokenNetwork, partner };
// wait for confirmation
const openPromise = asyncActionToPromise(channelOpen, meta, this.action$).then(({ txHash }) => {
onChange?.({ type: EventTypes.OPENED, payload: { txHash } });
return txHash; // pluck txHash
});
const openConfirmedPromise = asyncActionToPromise(channelOpen, meta, this.action$, true).then(({ txHash }) => {
onChange?.({ type: EventTypes.CONFIRMED, payload: { txHash } });
return txHash; // pluck txHash
});
let depositPromise;
if (deposit?.gt(0)) {
depositPromise = asyncActionToPromise(channelDeposit, meta, this.action$.pipe(
// ensure we only react on own deposit's responses
filter((action) => !channelDeposit.success.is(action) || action.payload.participant === this.address)), true).then(({ txHash }) => {
onChange?.({ type: EventTypes.DEPOSITED, payload: { txHash } });
return txHash; // pluck txHash
});
}
this.store.dispatch(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 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, } = {}) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(partner), [ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info);
const state = this.state;
const tokenNetwork = state.tokens[token];
assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
const deposit = decode(UInt(32), amount, ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info);
const meta = { tokenNetwork, partner };
const promise = asyncActionToPromise(channelDeposit, meta, this.action$.pipe(
// ensure we only react on own deposit's responses
filter((action) => !channelDeposit.success.is(action) || action.payload.participant === this.address)), true).then(({ txHash }) => txHash);
this.store.dispatch(channelDeposit.request({ deposit }, meta));
const depositTxHash = await promise;
if (confirmConfirmation) {
// wait twice confirmationBlocks for deposit or open tx
await 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) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(partner), [ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info);
const state = this.state;
const tokenNetwork = state.tokens[token];
assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
// try coop-settle first
try {
const channel = this.state.channels[channelKey({ tokenNetwork, partner })];
assert(channel, 'channel not found');
const { ownTotalWithdrawable: totalWithdraw } = channelAmounts(channel);
const expiration = Math.ceil(Date.now() / 1e3 + this.config.revealTimeout * this.config.expiryFactor);
const coopMeta = {
direction: Direction.SENT,
tokenNetwork,
partner,
totalWithdraw,
expiration,
};
const coopPromise = asyncActionToPromise(withdraw, coopMeta, this.action$, true).then(({ txHash }) => txHash);
this.store.dispatch(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 = asyncActionToPromise(channelClose, meta, this.action$, true).then(({ txHash }) => txHash);
this.store.dispatch(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) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(partner), [ErrorCodes.DTA_INVALID_ADDRESS, { partner }], this.log.info);
const state = this.state;
const tokenNetwork = state.tokens[token];
assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
assert(!this.config.autoSettle, ErrorCodes.CNL_SETTLE_AUTO_ENABLED, this.log.info);
const meta = { tokenNetwork, partner };
// wait for channel to become settleable
await lastValueFrom(waitChannelSettleable$(this.state$, meta));
// wait for the corresponding success or error action
const promise = asyncActionToPromise(channelSettle, meta, this.action$, true).then(({ txHash }) => txHash);
this.store.dispatch(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) {
assert(Address.is(address), [ErrorCodes.DTA_INVALID_ADDRESS, { address }], this.log.info);
const meta = { address };
const promise = asyncActionToPromise(matrixPresence, meta, this.action$);
this.store.dispatch(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 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 = {}) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(target), [ErrorCodes.DTA_INVALID_ADDRESS, { target }], this.log.info);
const tokenNetwork = this.state.tokens[token];
assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
const decodedValue = decode(UInt(32), value, ErrorCodes.DTA_INVALID_AMOUNT, this.log.info);
const paymentId = options.paymentId !== undefined
? decode(UInt(8), options.paymentId, ErrorCodes.DTA_INVALID_PAYMENT_ID, this.log.info)
: makePaymentId();
const paths = options.paths &&
decode(InputPaths, options.paths, ErrorCodes.DTA_INVALID_PATH, this.log.info);
const pfs = options.pfs && decode(PFS, options.pfs, ErrorCodes.DTA_INVALID_PFS, this.log.info);
assert(options.secret === undefined || Secret.is(options.secret), ErrorCodes.RDN_INVALID_SECRET, this.log.info);
assert(options.secrethash === undefined || Hash.is(options.secrethash), 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 : makeSecret());
const secrethash = options.secrethash || getSecrethash(secret);
assert(!secret || getSecrethash(secret) === secrethash, ErrorCodes.RDN_SECRET_SECRETHASH_MISMATCH, this.log.info);
const promise = firstValueFrom(this.action$.pipe(filter(isActionOf([transferSigned, transfer.failure])), filter(({ meta }) => meta.direction === Direction.SENT && meta.secrethash === secrethash), map((action) => {
if (transfer.failure.is(action))
throw action.payload;
return transferKey(action.meta);
})));
this.store.dispatch(transfer.request({
tokenNetwork,
target,
value: decodedValue,
paymentId,
secret,
resolved: false,
paths,
pfs,
lockTimeout: options.lockTimeout,
encryptSecret: options.encryptSecret,
}, { secrethash, direction: 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 } = transferKeyToMeta(transferKey);
let transferState = this.state.transfers[transferKey];
if (!transferState)
try {
transferState = decode(TransferState, await this.deps.db.get(transferKey));
}
catch (e) { }
assert(transferState, ErrorCodes.RDN_UNKNOWN_TRANSFER, this.log.info);
const raidenTransf = raidenTransfer(transferState);
// already completed/past transfer
if (raidenTransf.completed) {
if (raidenTransf.success)
return raidenTransf;
else
throw new RaidenError(ErrorCodes.XFER_ALREADY_COMPLETED, { status: raidenTransf.status });
}
// throws/rejects if a failure occurs
await asyncActionToPromise(transfer, { secrethash, direction }, this.action$);
const finalState = await firstValueFrom(this.state$.pipe(pluck('transfers', transferKey), 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 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 = {}) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(target), [ErrorCodes.DTA_INVALID_ADDRESS, { target }], this.log.info);
const tokenNetwork = this.state.tokens[token];
assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
const decodedValue = decode(UInt(32), value);
const pfs = options.pfs ? decode(PFS, options.pfs) : undefined;
const meta = { tokenNetwork, target, value: decodedValue };
const promise = asyncActionToPromise(pathFind, meta, this.action$).then(({ paths }) => paths);
this.store.dispatch(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) {
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
assert(Address.is(target), [ErrorCodes.DTA_INVALID_ADDRESS, { target }], this.log.info);
const tokenNetwork = this.state.tokens[token];
assert(tokenNetwork, ErrorCodes.RDN_UNKNOWN_TOKEN_NETWORK, this.log.info);
const decodedValue = decode(UInt(32), value);
const meta = { tokenNetwork, target, value: decodedValue };
const promise = asyncActionToPromise(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(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() {
assert(this.config.pfsMode !== PfsMode.disabled, ErrorCodes.PFS_DISABLED, this.log.info);
await this.synced;
const services = [...this.config.additionalServices];
if (this.config.pfsMode === PfsMode.auto)
services.push(...Object.keys(this.state.services));
return lastValueFrom(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
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
// Check whether we are on a test network
assert(this.deps.network.chainId !== 1, ErrorCodes.RDN_MINT_MAINNET, this.log.info);
const { signer, address } = chooseOnchainAccount(this.deps, this.config.subkey);
// Mint token
const customTokenContract = CustomToken__factory.connect(token, signer);
const beneficiary = to ?? address;
assert(Address.is(beneficiary), [ErrorCodes.DTA_INVALID_ADDRESS, { beneficiary }], this.log.info);
const value = decode(UInt(32), amount, ErrorCodes.DTA_INVALID_AMOUNT);
const [, receipt] = await lastValueFrom(transact(customTokenContract, 'mintFor', [value, beneficiary], this.deps, {
error: ErrorCodes.RDN_MINT_FAILED,
}));
// wait for a single block, so future calls will correctly pick value
await 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 = MaxUint256, tokenNetworkDepositLimit = MaxUint256) {
// Check whether address is valid
assert(Address.is(token), [ErrorCodes.DTA_INVALID_ADDRESS, { token }], this.log.info);
// Check whether we are on a test network
assert(this.deps.network.chainId !== 1, ErrorCodes.RDN_REGISTER_TOKEN_MAINNET, this.log.info);
const { signer } = chooseOnchainAccount(this.deps, this.config.subkey);
const tokenNetworkRegistry = getContractWithSigner(this.deps.registryContract, signer);
// Check whether token is already registered
await this.monitorToken(token).then((tokenNetwork) => {
throw new RaidenError(ErrorCodes.RDN_REGISTER_TOKEN_REGISTERED, { tokenNetwork });
}, () => undefined);
const [, receipt] = await lastValueFrom(transact(tokenNetworkRegistry, 'createERC20TokenNetwork', [token, channelParticipantDepositLimit, tokenNetworkDepositLimit], this.deps, { error: ErrorCodes.RDN_REGISTER_TOKEN_FAILED }));
await 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 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), 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 firstValueFrom(this.deps.latest$.pipe(pluck('udcDeposit', 'totalDeposit'), filter((deposit) => !!deposit && deposit.lt(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 = decode(UInt(32), amount, ErrorCodes.DTA_INVALID_DEPOSIT, this.log.info);
assert(deposit.gt(Zero), 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 = asyncActionToPromise(udcDeposit, meta, this.action$, false).then(({ txHash }) => onChange?.({ type: EventTypes.DEPOSITED, payload: { txHash } }));
this.store.dispatch(udcDeposit.request({ deposit }, meta));
const confirmed = asyncActionToPromise(udcDeposit, meta, this.action$, true).then(({ txHash }) => {
onChange?.({ type: 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 to send transactions
* Set this to true if one wants to force sending the transaction with the subkey
* @param options.gasPrice - Set to force a specific gasPrice; used to calculate transferable
* amount when transfering entire balance. If left unset, uses average network gasPrice
* @returns transaction hash
*/
async transferOnchainBalance(to, value = MaxUint256, { subkey, gasPrice: price } = {}) {
assert(Address.is(to), [ErrorCodes.DTA_INVALID_ADDRESS, { to }], this.log.info);
const { signer, address } = chooseOnchainAccount(this.deps, subkey ?? this.config.subkey);
// we use provider.getGasPrice directly in order to use the old gasPrice for txs, which
// allows us to predict exactly the final gasPrice and deplet balance
const gasPrice = price ? BigNumber.from(price) : await this.deps.provider.getGasPrice();
let curBalance, gasLimit;
for (let try_ = 0; !curBalance || !gasLimit; try_++) {
// curBalance may take some tries to be updated right after a tx
try {
curBalance = await this.getBalance(address);
gasLimit = await this.deps.provider.estimateGas({
from: address,
to,
value: curBalance,
});
}
catch (e) {
if (try_ >= 5)
throw e;
}
}
// transferableBalance is current balance minus the cost of a single transfer as per gasPrice
const transferableBalance = curBalance.sub(gasPrice.mul(gasLimit));
assert(transferableBalance.gt(Zero), [ErrorCodes.RDN_INSUFFICIENT_BALANCE, { transferableBalance }], this.log.warn);
// caps value to transferableBalance, so if it's too big, transfer all
const amount = transferableBalance.lte(value) ? transferableBalance : BigNumber.from(value);
const tx = await signer.sendTransaction({ to, value: amount, gasPrice, gasLimit });
const receipt = await tx.wait();
assert(receipt.status, ErrorCodes.RDN_TRANSFER_ONCHAIN_BALANCE_FAILED, this.log.info);
return tx.hash;
}
/**
* Transfer value tokens on-chain to address.
* If subkey is being used, use main account by default, or subkey account if 'subkey' is true
*
* @param token - Token address
* @param to - Recipient address
* @param value - Amount of tokens (in Wei) to transfer. Use ethers/utils::parseUnits 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 to send transactions
* Set this to true if one wants to force sending the transaction with the subkey
* @returns transaction hash
*/
asyn