UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

345 lines 13.4 kB
import { BigNumber } from '@ethersproject/bignumber'; import { arrayify, concat as concatBytes, hexlify } from '@ethersproject/bytes'; import { HashZero } from '@ethersproject/constants'; import { keccak256 } from '@ethersproject/keccak256'; import { randomBytes } from '@ethersproject/random'; import { sha256 } from '@ethersproject/sha2'; import { decrypt, encrypt } from 'eciesjs'; import * as t from 'io-ts'; import isEmpty from 'lodash/isEmpty'; import { defer, firstValueFrom, from, of } from 'rxjs'; import { filter, first, map, mergeMap } from 'rxjs/operators'; import { BalanceProofZero } from '../channels/types'; import { channelUniqueKey } from '../channels/utils'; import { Capabilities } from '../constants'; import { Metadata } from '../messages/types'; import { createBalanceHash, getBalanceProofFromEnvelopeMessage } from '../messages/utils'; import { validateAddressMetadata } from '../services/utils'; import { getCap } from '../transport/utils'; import { assert } from '../utils'; import { encode, jsonParse, jsonStringify } from '../utils/data'; import { decode, HexString, isntNil } from '../utils/types'; import { Direction, RaidenTransferStatus, RevealedSecret, TransferState } from './state'; /** * Get the locksroot of a given array of pending locks * On Alderaan, it's the keccak256 hash of the concatenation of the ordered locks data * * @param locks - Lock array to calculate the locksroot from * @returns hash of the locks array */ export function getLocksroot(locks) { const encoded = []; for (const lock of locks) encoded.push(encode(lock.expiration, 32), encode(lock.amount, 32), lock.secrethash); return keccak256(concatBytes(encoded)); } /** * Return the secrethash of a given secret * On Alderaan, the sha256 hash is used for the secret. * * @param secret - Secret to get the hash from * @returns hash of the secret */ export function getSecrethash(secret) { return sha256(secret); } /** * Generates a random secret of given length, as an HexString<32> * * @param length - of the secret to generate * @returns HexString<32> */ export function makeSecret(length = 32) { return hexlify(randomBytes(length)); } let lastPaymentId = 0; /** * Generates a unique 64bits payment identifier * * @returns Unique UInt<8> */ export function makePaymentId() { return BigNumber.from((lastPaymentId = Math.max(lastPaymentId + 1, Date.now()))); } let lastMsgId = 0; /** * Generates a unique 64bits message identifier * * @returns Unique UInt<8> */ export function makeMessageId() { return BigNumber.from((lastMsgId = Math.max(lastMsgId + 1, Date.now()))); } /** * Get a unique key for a tranfer state or TransferId * * @param state - transfer to get key from, or TransferId * @returns string containing a unique key for transfer */ export function transferKey(state) { if ('_id' in state) return state._id; return `${state.direction}:${state.secrethash}`; } const keyRe = new RegExp(`^(${Object.values(Direction).join('|')}):(0x[a-f0-9]{64})$`, 'i'); /** * Parse a transferKey into a TransferId object ({ secrethash, direction }) * * @param key - string to parse as transferKey * @returns secrethash, direction contained in transferKey */ export function transferKeyToMeta(key) { const match = key.match(keyRe); assert(match, 'Invalid transferKey format'); const [, direction, secrethash] = match; return { direction: direction, secrethash: secrethash }; } const statusesMap = { [RaidenTransferStatus.expired]: (t) => t.expiredProcessed?.ts, [RaidenTransferStatus.unlocked]: (t) => t.unlockProcessed?.ts, [RaidenTransferStatus.expiring]: (t) => t.expired?.ts, [RaidenTransferStatus.unlocking]: (t) => t.unlock?.ts, [RaidenTransferStatus.registered]: (t) => t.secretRegistered?.ts, [RaidenTransferStatus.revealed]: (t) => t.secretReveal?.ts, [RaidenTransferStatus.requested]: (t) => t.secretRequest?.ts, [RaidenTransferStatus.closed]: (t) => t.channelClosed?.ts, [RaidenTransferStatus.received]: (t) => t.transferProcessed?.ts, [RaidenTransferStatus.pending]: (t) => t.transfer.ts, }; /** * @param state - Transfer state * @returns Transfer's status */ export function transferStatus(state) { // order matters! from top to bottom priority, first match breaks loop for (const [s, g] of Object.entries(statusesMap)) { const ts = g(state); if (ts !== undefined) { return s; } } return RaidenTransferStatus.pending; } /** * @param state - Transfer state * @returns Whether the transfer is considered completed */ export function transferCompleted(state) { return !!(state.unlockProcessed || state.expiredProcessed || state.secretRegistered || state.channelSettled); } /** * Convert a TransferState to a public RaidenTransfer object * * @param state - RaidenState.sent value * @returns Public raiden sent transfer info object */ export function raidenTransfer(state) { const status = transferStatus(state); const startedAt = new Date(state.transfer.ts); const changedAt = new Date(statusesMap[status](state)); const transfer = state.transfer; const direction = state.direction; const value = transfer.lock.amount.sub(state.fee); const invalidSecretRequest = state.secretRequest && state.secretRequest.amount.lt(value); const success = state.secretReveal || state.unlock || state.secretRegistered ? true : invalidSecretRequest || state.expired || state.channelClosed ? false : undefined; const completed = transferCompleted(state); return { key: transferKey(state), secrethash: transfer.lock.secrethash, direction, status, initiator: transfer.initiator, partner: state.partner, target: transfer.target, metadata: transfer.metadata, paymentId: transfer.payment_identifier, chainId: transfer.chain_id.toNumber(), token: transfer.token, tokenNetwork: transfer.token_network_address, channelId: transfer.channel_identifier, value, fee: state.fee, amount: transfer.lock.amount, expiration: transfer.lock.expiration.toNumber(), startedAt, changedAt, success, completed, secret: state.secret, }; } /** * Look for a BalanceProof matching given balanceHash among EnvelopeMessages in transfers * * @param db - Database instance * @param channel - Channel key of hash * @param direction - Direction of transfers to search * @param balanceHash - Expected balanceHash * @returns BalanceProof matching balanceHash or undefined */ export function findBalanceProofMatchingBalanceHash$(db, channel, direction, balanceHash) { if (balanceHash === HashZero) return of(BalanceProofZero); return defer(() => // use db.storage directly instead of db.transfers to search on historical data db.find({ selector: { channel: channelUniqueKey(channel), direction } })).pipe(mergeMap(({ docs }) => from(docs)), mergeMap((doc) => { const transferState = decode(TransferState, doc); return from([transferState.transfer, transferState.unlock, transferState.expired]); }), filter(isntNil), map(getBalanceProofFromEnvelopeMessage), // will error observable if none matching is found first((bp) => createBalanceHash(bp) === balanceHash)); } /** * @param state - RaidenState or Observable of RaidenState to get transfer from * @param db - Try to fetch from db if not found on state * @param key - transferKey/_id to get * @returns Promise to TransferState */ export async function getTransfer(state, db, key) { if (typeof key !== 'string') key = transferKey(key); if (!('address' in state)) state = await firstValueFrom(state); const transfer = state.transfers[key]; if (transfer) return transfer; return decode(TransferState, await db.get(key)); } // a very simple/small subset of Metadata, to be used only to ensure metadata.routes[*].route array // the t.exact wrapper ensure unlisted properties are removed after decoding const RawMetadataCodec = t.exact(t.type({ routes: t.array(t.exact(t.type({ route: t.array(t.string), address_metadata: t.unknown }))), })); /** * Prune metadata route without changing any of the original encoding * A clear metadata route have partner (address after ours) as first hop in routes[*].route array. * To be used only if partner requires !Capabilities.IMMUTABLE_METADATA * * @param address - Our address * @param metadata - Metadata object * @returns A copy of metadata with routes cleared (i.e. partner as first/next address) */ export function clearMetadataRoute(address, metadata) { let decoded; try { decoded = decode(RawMetadataCodec, metadata); } catch (e) { return metadata; // if metadata isn't even RawMetadataCodec, just return it } const lowercaseAddr = address.toLowerCase(); return { ...decoded, routes: decoded.routes.map(({ route, ...rest }) => ({ ...rest, route: route.slice(route.findIndex((a) => a.toLowerCase() === lowercaseAddr) + 1), })), }; } /** * Contructs transfer.request's payload paramaters from received PFS's Paths * * @param paths - Paths array coming from PFS * @param target - presence of target address * @param encryptSecret - Try to encrypt this secret object to target * @returns Respective members of transfer.request's payload */ export function metadataFromPaths(paths, target, encryptSecret) { // paths may come with undesired parameters, so map&filter here before passing to metadata const routes = paths.map(({ path: route, fee: _, address_metadata }) => ({ route, ...(address_metadata && !isEmpty(address_metadata) ? { address_metadata } : {}), })); const viaPath = paths[0]; assert(viaPath, 'empty paths'); const fee = viaPath.fee; const partner = viaPath.path[1]; // we're first address in route, partner is 2nd assert(partner, 'empty route'); let partnerUserId; let partnerCaps; if (partner === target.meta.address) { partnerUserId = target.payload.userId; partnerCaps = target.payload.caps; } else { const partnerPresence = searchValidMetadata(viaPath.address_metadata, partner); partnerUserId = partnerPresence?.payload.userId; partnerCaps = partnerPresence?.payload.caps; } const via = { userId: partnerUserId }; let metadata = { routes }; // iff partner requires a clear route (to be first address), clear it; // in routes received from PFS, we're always first address and partner second if (!getCap(partnerCaps, Capabilities.IMMUTABLE_METADATA)) metadata = clearMetadataRoute(viaPath.path[0], metadata); else if (encryptSecret) { const encrypted = hexlify(encrypt(target.payload.pubkey, Buffer.from(jsonStringify(RevealedSecret.encode(encryptSecret))))); metadata = { ...metadata, secret: encrypted }; } return { resolved: true, metadata, fee, partner, ...via }; } const EncryptedSecretMetadata = t.type({ secret: HexString() }); /** * @param metadata - Undecoded metadata * @param transfer - Transfer info * @param transfer."0" - Transfer's secrethash * @param transfer."1" - Transfer's effective received amount * @param transfer."2" - Transfer's paymendId * @param signer - Our effective signer (with `privateKey`) * @returns Secret, if decryption and all validations pass */ export function decryptSecretFromMetadata(metadata, [secrethash, amount, paymentId], signer) { const privkey = signer.privateKey; if (!privkey) return; try { const encrypted = decode(EncryptedSecretMetadata, metadata).secret; const decrypted = decrypt(privkey, Buffer.from(arrayify(encrypted))).toString(); const parsed = decode(RevealedSecret, jsonParse(decrypted)); assert(amount.gte(parsed.amount) && getSecrethash(parsed.secret) === secrethash); assert(!paymentId || !parsed.payment_identifier || paymentId.eq(parsed.payment_identifier)); return parsed.secret; } catch (e) { } } /** * @param addressMetadata - metadata's address_metadata mapping * @param address - Address to search and validate * @returns AddressMetadata of given address */ export function searchValidMetadata(addressMetadata, address) { // support address_metadata keys being both lowercase and checksummed addresses const metadata = addressMetadata?.[address] ?? addressMetadata?.[address.toLowerCase()]; if (metadata) { const presence = validateAddressMetadata(metadata, address); if (presence) return presence; } } /** * @param metadata - Transfer metadata to search on * @param address - Address metadata to search for * @returns Via object or undefined */ export function searchValidViaAddress(metadata, address) { let userId; let decoded; try { decoded = decode(Metadata, metadata); } catch (e) { } if (!decoded || !address) return; for (const { address_metadata } of decoded.routes) { if ((userId = searchValidMetadata(address_metadata, address)?.payload.userId)) return { userId }; } } //# sourceMappingURL=utils.js.map