raiden-ts
Version:
Raiden Light Client Typescript/Javascript SDK
512 lines (510 loc) • 25.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeDependencies = exports.waitChannelSettleable$ = exports.getState = exports.makeTokenInfoGetter = exports.makeSyncedPromise = exports.getUdcBalance = exports.waitConfirmation = exports.getContractWithSigner = exports.chooseOnchainAccount = exports.mapRaidenChannels = exports.initTransfers$ = exports.getSigner = void 0;
const abstract_signer_1 = require("@ethersproject/abstract-signer");
const constants_1 = require("@ethersproject/constants");
const providers_1 = require("@ethersproject/providers");
const sha2_1 = require("@ethersproject/sha2");
const strings_1 = require("@ethersproject/strings");
const wallet_1 = require("@ethersproject/wallet");
const fs_1 = require("fs");
const constant_1 = __importDefault(require("lodash/constant"));
const memoize_1 = __importDefault(require("lodash/memoize"));
const loglevel_1 = __importDefault(require("loglevel"));
const path_1 = __importDefault(require("path"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const actions_1 = require("./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 deployment_arbitrum_one_json_1 = __importDefault(require("./deployment/deployment_arbitrum-one.json"));
const deployment_goerli_unstable_json_1 = __importDefault(require("./deployment/deployment_goerli_unstable.json"));
const deployment_rinkeby_arbitrum_json_1 = __importDefault(require("./deployment/deployment_rinkeby-arbitrum.json"));
const deployment_services_arbitrum_one_json_1 = __importDefault(require("./deployment/deployment_services_arbitrum-one.json"));
const deployment_services_goerli_unstable_json_1 = __importDefault(require("./deployment/deployment_services_goerli_unstable.json"));
const deployment_services_rinkeby_arbitrum_json_1 = __importDefault(require("./deployment/deployment_services_rinkeby-arbitrum.json"));
const state_2 = require("./state");
const types_1 = require("./transfers/mediate/types");
const state_3 = require("./transfers/state");
const utils_3 = require("./transfers/utils");
const types_2 = require("./types");
const utils_4 = require("./utils");
const actions_2 = require("./utils/actions");
const data_1 = require("./utils/data");
const error_1 = require("./utils/error");
const ethers_1 = require("./utils/ethers");
const lru_1 = require("./utils/lru");
const rx_1 = require("./utils/rx");
const types_3 = require("./utils/types");
/**
* Returns contract information depending on the passed [[Network]].
* The deployment info of known networks are embedded at build-time. In case it can't parse as one
* of those, we try to use NodeJS's `fs` (or compatible shims) utilities to read json directly
* from the `deployment` dist folder.
*
* @param network - Current network, as detected by ether's Provider (see @ethersproject/networks)
* @returns deployed contract information of the network
*/
function getContracts(network) {
let info;
switch (network.name) {
// known networks go here; using imported JSONs instead of fs read embeds it at compile-time
// and should also work for bundled builds, which breaks more easily when trying to read at
// runtime due to missing or wrong path of JSONs in dist folders, but we still try that as
// fallback nin the `default` case
case 'arbitrum-rinkeby':
info = { ...deployment_rinkeby_arbitrum_json_1.default.contracts, ...deployment_services_rinkeby_arbitrum_json_1.default.contracts };
break;
case 'arbitrum':
info = { ...deployment_arbitrum_one_json_1.default.contracts, ...deployment_services_arbitrum_one_json_1.default.contracts };
break;
case 'goerli':
info = { ...deployment_goerli_unstable_json_1.default.contracts, ...deployment_services_goerli_unstable_json_1.default.contracts };
break;
default:
try {
info = {
...JSON.parse((0, fs_1.readFileSync)(path_1.default.join(__dirname, 'deployment', `deployment_${(0, ethers_1.getNetworkName)(network)}.json`), 'utf-8')),
...JSON.parse((0, fs_1.readFileSync)(path_1.default.join(__dirname, 'deployment', `deployment_services_${(0, ethers_1.getNetworkName)(network)}.json`), 'utf-8')),
};
}
catch (e) {
throw new error_1.RaidenError(error_1.ErrorCodes.RDN_UNRECOGNIZED_NETWORK, { network });
}
}
return (0, types_3.decode)(types_2.ContractsInfo, info);
}
/**
* Generate, sign and return a subkey from provided main account
*
* @param network - Network to include in message
* @param main - Main signer to derive subkey from
* @param originUrl - URL of the origin to generate the subkey for
* @returns Subkey's signer & address
*/
async function genSubkey(network, main, originUrl) {
const url = originUrl ?? globalThis.location?.origin ?? 'unknown';
const message = `=== RAIDEN SUBKEY GENERATION ===
Network: ${(0, ethers_1.getNetworkName)(network).toUpperCase()}
Raiden dApp URL: ${url}
WARNING: ensure this signature is being requested from Raiden dApp running at URL above by comparing it to your browser's url bar.
Signing this message at any other address WILL give it FULL control of this subkey's funds, tokens and Raiden channels!`;
const signature = await main.signMessage((0, strings_1.toUtf8Bytes)(message));
const pk = (0, sha2_1.sha256)(signature);
const signer = new wallet_1.Wallet(pk, main.provider);
return { signer, address: signer.address };
}
/**
* Returns a [[Signer]] based on the `account` and `provider`.
* Throws an exception if the `account` is not a valid address or private key.
*
* @param account - an account used for signing
* @param provider - a provider
* @param subkey - Whether to generate a subkey
* @param subkeyOriginUrl - URL of the origin to generate a subkey for
* @returns a [[Signer]] or [[Wallet]] that can be used for signing
*/
const getSigner = async (account, provider, subkey, subkeyOriginUrl) => {
let signer;
let address;
let main;
if (abstract_signer_1.Signer.isSigner(account)) {
if (account.provider === provider) {
signer = account;
}
else if (account instanceof wallet_1.Wallet) {
signer = account.connect(provider);
}
else {
throw new error_1.RaidenError(error_1.ErrorCodes.RDN_SIGNER_NOT_CONNECTED, {
account: account.toString(),
provider: provider.toString(),
});
}
address = (await signer.getAddress());
}
else if (typeof account === 'number') {
// index of account in provider
signer = provider.getSigner(account);
address = (await signer.getAddress());
}
else if (types_3.Address.is(account)) {
// address
const accounts = await provider.listAccounts();
if (!accounts.includes(account)) {
throw new error_1.RaidenError(error_1.ErrorCodes.RDN_ACCOUNT_NOT_FOUND, {
account,
accounts: JSON.stringify(accounts),
});
}
signer = provider.getSigner(account);
address = account;
}
else if (types_3.PrivateKey.is(account)) {
// private key
signer = new wallet_1.Wallet(account, provider);
address = signer.address;
}
else {
throw new error_1.RaidenError(error_1.ErrorCodes.RDN_STRING_ACCOUNT_INVALID);
}
if (subkey) {
main = { signer, address };
({ signer, address } = await genSubkey(await provider.getNetwork(), main.signer, subkeyOriginUrl));
}
return { signer, address, main };
};
exports.getSigner = getSigner;
/**
* Provides a live stream of transfer documents containing transfer updates
* If you want pagination, use [[getTransfers]] instead
*
* @param db - Database instance
* @returns observable of sent and completed Raiden transfers
*/
function initTransfers$(db) {
return (0, utils_2.changes$)(db, {
since: 0,
live: true,
include_docs: true,
selector: { 'transfer.ts': { $gt: 0 } },
}).pipe((0, operators_1.map)(({ doc }) => (0, utils_3.raidenTransfer)((0, types_3.decode)(state_3.TransferState, doc))));
}
exports.initTransfers$ = initTransfers$;
/**
* Transforms the redux channel state to [[RaidenChannels]]
*
* @param channels - RaidenState.channels
* @returns Raiden public channels mapping
*/
const mapRaidenChannels = (channels) => Object.values(channels).reduce((acc, channel) => {
const amounts = (0, utils_1.channelAmounts)(channel);
const raidenChannel = {
state: channel.state,
id: channel.id,
token: channel.token,
tokenNetwork: channel.tokenNetwork,
openBlock: channel.openBlock,
closeBlock: 'closeBlock' in channel ? channel.closeBlock : undefined,
partner: channel.partner.address,
balance: amounts.ownBalance,
capacity: amounts.ownCapacity,
...amounts,
};
return {
...acc,
[channel.token]: {
...acc[channel.token],
[channel.partner.address]: raidenChannel,
},
};
}, {});
exports.mapRaidenChannels = mapRaidenChannels;
/**
* Return signer & address to use for on-chain txs depending on subkey param
*
* @param deps - RaidenEpicDeps subset
* @param deps.signer - Signer instance
* @param deps.address - Own address
* @param deps.main - Main signer/address, if any
* @param subkey - Whether to prefer the subkey or the main key
* @returns Signer & Address to use for on-chain operations
*/
function chooseOnchainAccount({ signer, address, main, }, subkey) {
if (main && !subkey)
return main;
return { signer, address };
}
exports.chooseOnchainAccount = chooseOnchainAccount;
/**
* Returns a contract instance with attached signer
*
* @param contract - Contract instance
* @param signer - Signer to use on contract
* @returns contract instance with signer
*/
function getContractWithSigner(contract, signer) {
if (contract.signer === signer)
return contract;
return contract.connect(signer);
}
exports.getContractWithSigner = getContractWithSigner;
/**
* Waits for receipt to have at least `confBlocks` confirmations; resolves immediately if already;
* throws if it gets removed by a reorg.
*
* @param receipt - Receipt to wait for confirmation
* @param deps - RaidenEpicDeps
* @param deps.latest$ - Latest observable
* @param deps.config$ - Config observable
* @param deps.provider - Eth provider
* @param confBlocks - Confirmation blocks, defaults to `config.confirmationBlocks`
* @returns Promise to final blockNumber of transaction
*/
async function waitConfirmation(receipt, { latest$, config$, provider }, confBlocks) {
const txBlock = receipt.blockNumber;
const txHash = receipt.transactionHash;
return (0, rxjs_1.firstValueFrom)(latest$.pipe((0, rx_1.pluckDistinct)('state', 'blockNumber'), (0, operators_1.withLatestFrom)(config$), (0, operators_1.filter)(([blockNumber, { confirmationBlocks }]) => txBlock + (confBlocks ?? confirmationBlocks) <= blockNumber), (0, operators_1.exhaustMap)(([blockNumber, { confirmationBlocks }]) => (0, rxjs_1.defer)(async () => provider.getTransactionReceipt(txHash)).pipe((0, operators_1.map)((receipt) => {
if (receipt?.confirmations &&
receipt.confirmations >= (confBlocks ?? confirmationBlocks))
return receipt.blockNumber;
else if (txBlock + 2 * (confBlocks ?? confirmationBlocks) < blockNumber)
throw new error_1.RaidenError(error_1.ErrorCodes.RDN_TRANSACTION_REORG, {
transactionHash: txHash,
});
}))), (0, operators_1.filter)(types_3.isntNil)));
}
exports.waitConfirmation = waitConfirmation;
/**
* Construct entire ContractsInfo using UserDeposit contract address as entrypoint
*
* @param provider - Ethers provider to use to fetch contracts data
* @param userDeposit - UserDeposit contract address as entrypoint
* @param fromBlock - If specified, uses this as initial scanning block
* @returns contracts info, with blockNumber as block of first registered tokenNetwork
*/
async function fetchContractsInfo(provider, userDeposit, fromBlock) {
const userDepositContract = contracts_1.UserDeposit__factory.connect(userDeposit, provider);
const monitoringService = (await userDepositContract.msc_address());
const monitoringServiceContract = contracts_1.MonitoringService__factory.connect(monitoringService, provider);
const tokenNetworkRegistry = (await monitoringServiceContract.token_network_registry());
const tokenNetworkRegistryContract = contracts_1.TokenNetworkRegistry__factory.connect(tokenNetworkRegistry, provider);
const secretRegistry = (await tokenNetworkRegistryContract.secret_registry_address());
const serviceRegistry = (await monitoringServiceContract.service_registry());
const toBlock = await provider.getBlockNumber();
const firstBlock = fromBlock ||
(await (0, rxjs_1.firstValueFrom)((0, ethers_1.getLogsByChunk$)(provider, {
...tokenNetworkRegistryContract.filters.TokenNetworkCreated(null, null),
fromBlock: 1,
toBlock,
}).pipe((0, operators_1.pluck)('blockNumber'), (0, operators_1.filter)(types_3.isntNil)), { defaultValue: toBlock }));
const oneToN = (await userDepositContract.one_to_n_address());
return {
TokenNetworkRegistry: { address: tokenNetworkRegistry, block_number: firstBlock },
ServiceRegistry: { address: serviceRegistry, block_number: firstBlock },
UserDeposit: { address: userDeposit, block_number: firstBlock },
SecretRegistry: { address: secretRegistry, block_number: firstBlock },
MonitoringService: { address: monitoringService, block_number: firstBlock },
OneToN: { address: oneToN, block_number: firstBlock },
};
}
/**
* Resolves to our current UDC balance, as seen from [[monitorUdcBalanceEpic]]
*
* @param latest$ - Latest observable
* @returns Promise to our current UDC balance
*/
async function getUdcBalance(latest$) {
return (0, rxjs_1.firstValueFrom)(latest$.pipe((0, operators_1.pluck)('udcDeposit', 'balance'), (0, operators_1.filter)((balance) => !!balance && balance.lt(constants_1.MaxUint256))));
}
exports.getUdcBalance = getUdcBalance;
/**
* @param action$ - Observable of RaidenActions
* @returns Promise which resolves when Raiden is synced
*/
function makeSyncedPromise(action$) {
return (0, rxjs_1.firstValueFrom)(action$.pipe((0, operators_1.first)((0, actions_2.isActionOf)([actions_1.raidenSynced, actions_1.raidenShutdown])), (0, operators_1.map)((action) => {
if (actions_1.raidenShutdown.is(action)) {
// don't reject if not stopped by an error
if (Object.values(constants_2.ShutdownReason).some((reason) => reason === action.payload.reason))
return;
throw action.payload;
}
return action.payload;
})));
}
exports.makeSyncedPromise = makeSyncedPromise;
/**
* @param deps - Epics dependencies
* @param deps.log - Logger instance
* @param deps.getTokenContract - Token contract factory/getter
* @returns Memoized function to fetch token info
*/
function makeTokenInfoGetter({ log, getTokenContract, }) {
return (0, memoize_1.default)(async function getTokenInfo(token) {
(0, utils_4.assert)(types_3.Address.is(token), [error_1.ErrorCodes.DTA_INVALID_ADDRESS, { token }], log.info);
const tokenContract = getTokenContract(token);
const [totalSupply, decimals, name, symbol] = await Promise.all([
tokenContract.callStatic.totalSupply(),
tokenContract.callStatic.decimals(),
tokenContract.callStatic.name().catch((0, constant_1.default)(undefined)),
tokenContract.callStatic.symbol().catch((0, constant_1.default)(undefined)),
]);
return { totalSupply, decimals, name, symbol };
});
}
exports.makeTokenInfoGetter = makeTokenInfoGetter;
function validateDump(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dump, { address, network, udc }) {
const meta = dump[0];
(0, utils_4.assert)(meta?._id === '_meta', error_1.ErrorCodes.RDN_STATE_MIGRATION);
(0, utils_4.assert)(meta.address === address, error_1.ErrorCodes.RDN_STATE_ADDRESS_MISMATCH);
(0, utils_4.assert)(meta.udc === udc, error_1.ErrorCodes.RDN_STATE_NETWORK_MISMATCH);
(0, utils_4.assert)(meta.network === network.chainId, error_1.ErrorCodes.RDN_STATE_NETWORK_MISMATCH);
(0, utils_4.assert)(dump.find((l) => l._id === 'state.address')?.value === address, error_1.ErrorCodes.RDN_STATE_ADDRESS_MISMATCH);
(0, utils_4.assert)(dump.find((l) => l._id === 'state.chainId')?.value === network.chainId, error_1.ErrorCodes.RDN_STATE_NETWORK_MISMATCH);
(0, utils_4.assert)(dump.find((l) => l._id === 'state.contracts')?.value?.UserDeposit?.address === udc, error_1.ErrorCodes.RDN_STATE_NETWORK_MISMATCH);
}
function getUdcAndBlock(contracts) {
if (typeof contracts === 'object')
return [contracts.UserDeposit.address, contracts.UserDeposit.block_number];
const match = contracts.match(/^(0x[0-9a-f]{40})(?::(\d+))?$/i);
(0, utils_4.assert)(match && types_3.Address.is(match[1]), [
error_1.ErrorCodes.DTA_INVALID_ADDRESS,
{ contractsOrUserDepositAddress: contracts },
]);
return [match[1], match[2] ? +match[2] : 1];
}
/**
* Loads state from `storageOrState`. Returns the initial [[RaidenState]] if
* `storageOrState` does not exist.
*
* @param deps - Partial epics dependencies-like object
* @param deps.provider - Provider instance
* @param deps.address - current address of the signer
* @param deps.network - current network
* @param deps.log - Logger instance
* @param contractsOrUDCAddress - ContractsInfo object or UDC address
* @param storage - diverse storage related parameters to load from and save to
* @param storage.state - Uploaded state: replaces database state; must be newer than database
* @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;
* @returns database and RaidenDoc object
*/
async function getState({ provider, address, network, log, }, contractsOrUDCAddress = getContracts(network),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
storage = {}) {
const [udc, fromBlock] = getUdcAndBlock(contractsOrUDCAddress);
const dbName = ['raiden', (0, ethers_1.getNetworkName)(network), udc, address].join('_');
let db;
const { state: stateDump, ...opts } = storage;
let dump = stateDump;
// PouchDB configs are passed as custom database constructor using PouchDB.defaults
const dbCtor = await (0, utils_2.getDatabaseConstructorFromOptions)({ ...opts, log });
if (dump) {
if (typeof dump === 'string')
dump = (0, data_1.jsonParse)(dump);
// perform some early simple validation on dump before persisting it in database
validateDump(dump, { address, network, udc });
db = await utils_2.replaceDatabase.call(dbCtor, dump, dbName);
// only if succeeds:
}
else {
db = await utils_2.migrateDatabase.call(dbCtor, dbName);
}
let state = await (0, utils_2.getRaidenState)(db);
if (!state) {
let contractsInfo = contractsOrUDCAddress;
if (typeof contractsInfo === 'string') {
log.warn('fetching contractsInfo from UDC entrypoint', { udc, fromBlock });
contractsInfo = await fetchContractsInfo(provider, udc, fromBlock);
}
state = (0, state_2.makeInitialState)({ network, address, contractsInfo });
await (0, utils_2.putRaidenState)(db, state);
}
else {
state = (0, types_3.decode)(state_2.RaidenState, state);
}
return { db, state };
}
exports.getState = getState;
const settleableStates = [state_1.ChannelState.settleable, state_1.ChannelState.settling];
const preSettleableStates = [state_1.ChannelState.closed, ...settleableStates];
/**
* Waits for channel to become settleable
*
* Errors if channel doesn't exist or isn't closed, settleable or settling (states which precede
* or are considered settleable)
*
* @param state$ - Observable of RaidenStates
* @param meta - meta of channel for which to wait
* @returns Observable which waits until channel becomes settleable
*/
function waitChannelSettleable$(state$, meta) {
return state$.pipe((0, operators_1.first)(), (0, operators_1.mergeMap)(({ channels }) => {
const channel = channels[(0, utils_1.channelKey)(meta)];
(0, utils_4.assert)(channel && preSettleableStates.includes(channel.state), error_1.ErrorCodes.CNL_NO_SETTLEABLE_OR_SETTLING_CHANNEL_FOUND);
return state$.pipe((0, rx_1.pluckDistinct)('channels', (0, utils_1.channelKey)(meta)));
}), (0, operators_1.first)((channel) => settleableStates.includes(channel.state)));
}
exports.waitChannelSettleable$ = waitChannelSettleable$;
/**
* Make a getBlockTimestamp function which caches the returned observable for a given blockNumber,
* retries in case of errors and clears the cache in case of permanent failure;
*
* @param provider - provider instance to get block info from
* @param maxErrors - maximum errors to retry
* @returns cached observable which emits block timestamp (in seconds) once and completes
*/
function makeBlockTimestampGetter(provider, maxErrors = 3) {
const cache = new lru_1.LruCache(128);
return function getBlockTimestamp(block) {
let cached = cache.get(block);
if (!cached) {
cached = (0, rxjs_1.defer)(async () => provider.getBlock(block)).pipe((0, operators_1.map)(({ timestamp }) => {
(0, utils_4.assert)(timestamp, ['no timestamp in block', { block }]);
return timestamp;
}), (0, operators_1.retryWhen)((err$) => err$.pipe((0, operators_1.mergeMap)((err, i) => {
if (i >= maxErrors)
throw err;
return (0, rxjs_1.timer)(provider.pollingInterval);
}))), (0, operators_1.tap)({ error: () => cache.delete(block) }), (0, operators_1.shareReplay)({ bufferSize: 1, refCount: false }));
cache.set(block, cached);
}
return cached;
};
}
/**
* Helper function to create the RaidenEpicDeps dependencies object for Raiden Epics
*
* @param state - Initial/previous RaidenState
* @param config - defaultConfig overwrites
* @param opts - Options
* @param opts.signer - Signer holding raiden account connected to a JsonRpcProvider
* @param opts.contractsInfo - Object holding deployment information from Raiden contracts on
* current network
* @param opts.db - Database instance
* @param opts.main - Main account object, set when using a subkey as raiden signer
* @returns Constructed epics dependencies object
*/
function makeDependencies(state, config, { signer, contractsInfo, db, main, }) {
(0, utils_4.assert)(signer.provider && signer.provider instanceof providers_1.JsonRpcProvider && signer.provider.network, 'Signer must be connected to a JsonRpcProvider');
const latest$ = new rxjs_1.ReplaySubject(1);
const config$ = latest$.pipe((0, rx_1.pluckDistinct)('config'));
const registryContract = contracts_1.TokenNetworkRegistry__factory.connect(contractsInfo.TokenNetworkRegistry.address, main?.signer ?? signer);
return {
latest$,
config$,
matrix$: new rxjs_1.AsyncSubject(),
signer,
provider: signer.provider,
network: signer.provider.network,
address: state.address,
log: loglevel_1.default.getLogger(`raiden:${state.address}`),
defaultConfig: (0, config_1.makeDefaultConfig)({ network: signer.provider.network }, config),
contractsInfo,
registryContract,
getTokenNetworkContract: (0, memoize_1.default)((address) => contracts_1.TokenNetwork__factory.connect(address, main?.signer ?? signer)),
getTokenContract: (0, memoize_1.default)((address) => contracts_1.HumanStandardToken__factory.connect(address, main?.signer ?? signer)),
serviceRegistryContract: contracts_1.ServiceRegistry__factory.connect(contractsInfo.ServiceRegistry.address, main?.signer ?? signer),
userDepositContract: contracts_1.UserDeposit__factory.connect(contractsInfo.UserDeposit.address, main?.signer ?? signer),
secretRegistryContract: contracts_1.SecretRegistry__factory.connect(contractsInfo.SecretRegistry.address, main?.signer ?? signer),
monitoringServiceContract: contracts_1.MonitoringService__factory.connect(contractsInfo.MonitoringService.address, main?.signer ?? signer),
main,
db,
init$: new rxjs_1.ReplaySubject(),
mediationFeeCalculator: types_1.standardCalculator,
getBlockTimestamp: makeBlockTimestampGetter(signer.provider),
};
}
exports.makeDependencies = makeDependencies;
//# sourceMappingURL=helpers.js.map