UNPKG

raiden-ts

Version:

Raiden Light Client Typescript/Javascript SDK

376 lines 15.6 kB
import { arrayify, concat as concatBytes, hexlify } from '@ethersproject/bytes'; import { HashZero } from '@ethersproject/constants'; import { keccak256 } from '@ethersproject/keccak256'; import { encode as rlpEncode } from '@ethersproject/rlp'; import { toUtf8Bytes } from '@ethersproject/strings'; import { verifyMessage } from '@ethersproject/wallet'; import { canonicalize } from 'json-canonicalize'; import logging from 'loglevel'; import { Capabilities, LocksrootZero } from '../constants'; import { getCap } from '../transport/utils'; import { assert } from '../utils'; import { encode, jsonParse, jsonStringify } from '../utils/data'; import { ErrorCodes } from '../utils/error'; import { decode, Signature, Signed } from '../utils/types'; import { messageReceived } from './actions'; import { Message, MessageType, Metadata } from './types'; const CMDIDs = { [MessageType.DELIVERED]: 12, [MessageType.PROCESSED]: 0, [MessageType.SECRET_REQUEST]: 3, [MessageType.SECRET_REVEAL]: 11, [MessageType.LOCKED_TRANSFER]: 7, [MessageType.UNLOCK]: 4, [MessageType.LOCK_EXPIRED]: 13, [MessageType.WITHDRAW_REQUEST]: 15, [MessageType.WITHDRAW_CONFIRMATION]: 16, [MessageType.WITHDRAW_EXPIRED]: 17, [MessageType.PFS_CAPACITY_UPDATE]: -1, [MessageType.PFS_FEE_UPDATE]: -1, [MessageType.MONITOR_REQUEST]: -1, }; // raiden_contracts.constants.MessageTypeId export var MessageTypeId; (function (MessageTypeId) { MessageTypeId[MessageTypeId["BALANCE_PROOF"] = 1] = "BALANCE_PROOF"; MessageTypeId[MessageTypeId["BALANCE_PROOF_UPDATE"] = 2] = "BALANCE_PROOF_UPDATE"; MessageTypeId[MessageTypeId["WITHDRAW"] = 3] = "WITHDRAW"; MessageTypeId[MessageTypeId["COOP_SETTLE"] = 4] = "COOP_SETTLE"; MessageTypeId[MessageTypeId["IOU"] = 5] = "IOU"; MessageTypeId[MessageTypeId["MS_REWARD"] = 6] = "MS_REWARD"; })(MessageTypeId || (MessageTypeId = {})); /** * Create the hash of Metadata structure. * * @param metadata - The LockedTransfer metadata * @returns Hash of the metadata. */ function createMetadataHash(metadata) { return keccak256(toUtf8Bytes(canonicalize(metadata))); } /** * Returns a balance_hash from transferred&locked amounts & locksroot * * @param bp - BalanceProof-like object * @param bp.transferredAmount - balanceProof's transferredAmount * @param bp.lockedAmount - balanceProof's lockedAmount * @param bp.locksroot - balanceProof's locksroot * @returns Hash of the balance */ export function createBalanceHash({ transferredAmount, lockedAmount, locksroot, }) { let hash = HashZero; if (!transferredAmount.isZero() || !lockedAmount.isZero() || (locksroot !== HashZero && locksroot !== LocksrootZero)) hash = keccak256(concatBytes([ encode(transferredAmount, 32), encode(lockedAmount, 32), encode(locksroot, 32), ])); return hash; } /** * Create the messageHash/additionalHash for a given EnvelopeMessage * * @param message - EnvelopeMessage to pack * @returns Hash of the message pack */ export function createMessageHash(message) { let hash; switch (message.type) { case MessageType.LOCKED_TRANSFER: // hash of packed representation of the whole message hash = keccak256(concatBytes([ encode(CMDIDs[message.type], 1), encode(message.message_identifier, 8), encode(message.payment_identifier, 8), encode(message.lock.expiration, 32), encode(message.token, 20), encode(message.recipient, 20), encode(message.target, 20), encode(message.initiator, 20), encode(message.lock.secrethash, 32), encode(message.lock.amount, 32), createMetadataHash(message.metadata), ])); break; case MessageType.UNLOCK: hash = keccak256(concatBytes([ encode(CMDIDs[message.type], 1), encode(message.message_identifier, 8), encode(message.payment_identifier, 8), encode(message.secret, 32), ])); break; case MessageType.LOCK_EXPIRED: hash = keccak256(concatBytes([ encode(CMDIDs[message.type], 1), encode(message.message_identifier, 8), encode(message.recipient, 20), encode(message.secrethash, 32), ])); break; } return hash; } /** * Pack a message in a hex-string format, **without** signature * This packed hex-byte-array can then be used for signing. * On Raiden python client, this is the output of `_data_to_sign` method of the messages, as the * actual packed encoding was once used for binary transport protocols, but nowadays is used only * for generating data to be signed, which is the purpose of our implementation. * * @param message - Message to be packed * @returns HexBytes hex-encoded string data representing message in binary format */ export function packMessage(message) { switch (message.type) { case MessageType.DELIVERED: return hexlify(concatBytes([ encode(CMDIDs[message.type], 1), encode(0, 3), encode(message.delivered_message_identifier, 8), ])); case MessageType.PROCESSED: return hexlify(concatBytes([ encode(CMDIDs[message.type], 1), encode(0, 3), encode(message.message_identifier, 8), ])); case MessageType.LOCKED_TRANSFER: case MessageType.UNLOCK: case MessageType.LOCK_EXPIRED: { const additionalHash = createMessageHash(message), balanceHash = createBalanceHash({ transferredAmount: message.transferred_amount, lockedAmount: message.locked_amount, locksroot: message.locksroot, }); return hexlify(concatBytes([ encode(message.token_network_address, 20), encode(message.chain_id, 32), encode(MessageTypeId.BALANCE_PROOF, 32), encode(message.channel_identifier, 32), encode(balanceHash, 32), encode(message.nonce, 32), encode(additionalHash, 32), ])); } case MessageType.SECRET_REQUEST: return hexlify(concatBytes([ encode(CMDIDs[message.type], 1), encode(0, 3), encode(message.message_identifier, 8), encode(message.payment_identifier, 8), encode(message.secrethash, 32), encode(message.amount, 32), encode(message.expiration, 32), ])); case MessageType.SECRET_REVEAL: return hexlify(concatBytes([ encode(CMDIDs[message.type], 1), encode(0, 3), encode(message.message_identifier, 8), encode(message.secret, 32), ])); case MessageType.WITHDRAW_REQUEST: case MessageType.WITHDRAW_CONFIRMATION: return hexlify(concatBytes([ encode(message.token_network_address, 20), encode(message.chain_id, 32), encode(MessageTypeId.WITHDRAW, 32), encode(message.channel_identifier, 32), encode(message.participant, 20), encode(message.total_withdraw, 32), encode(message.expiration, 32), ])); case MessageType.WITHDRAW_EXPIRED: return hexlify(concatBytes([ encode(CMDIDs[message.type], 1), encode(0, 3), encode(message.nonce, 32), encode(message.message_identifier, 8), encode(message.token_network_address, 20), encode(message.chain_id, 32), encode(MessageTypeId.WITHDRAW, 32), encode(message.channel_identifier, 32), encode(message.participant, 20), encode(message.total_withdraw, 32), encode(message.expiration, 32), ])); case MessageType.PFS_CAPACITY_UPDATE: return hexlify(concatBytes([ encode(message.canonical_identifier.chain_identifier, 32), encode(message.canonical_identifier.token_network_address, 20), encode(message.canonical_identifier.channel_identifier, 32), encode(message.updating_participant, 20), encode(message.other_participant, 20), encode(message.updating_nonce, 8), encode(message.other_nonce, 8), encode(message.updating_capacity, 32), encode(message.other_capacity, 32), encode(message.reveal_timeout, 32), ])); case MessageType.PFS_FEE_UPDATE: return hexlify(concatBytes([ encode(message.canonical_identifier.chain_identifier, 32), encode(message.canonical_identifier.token_network_address, 20), encode(message.canonical_identifier.channel_identifier, 32), encode(message.updating_participant, 20), encode(message.fee_schedule.cap_fees, 1), encode(message.fee_schedule.flat, 32), encode(message.fee_schedule.proportional, 32), rlpEncode(message.fee_schedule.imbalance_penalty ? message.fee_schedule.imbalance_penalty.map((point) => // RLP integer 0 must be encoded as the empty bytestring point.map((p) => (p.isZero() ? '0x' : p.toHexString()))) : '0x'), toUtf8Bytes(message.timestamp), ])); // variable size of fee_schedule.imbalance_penalty rlpEncoding, when not null case MessageType.MONITOR_REQUEST: return hexlify(concatBytes([ encode(message.monitoring_service_contract_address, 20), encode(message.balance_proof.chain_id, 32), encode(MessageTypeId.MS_REWARD, 32), encode(message.balance_proof.token_network_address, 20), encode(message.non_closing_participant, 20), encode(message.non_closing_signature, 65), encode(message.reward_amount, 32), ])); } } /** * Typeguard to check if a message contains a valid signature * * @param message - May or may not be a signed message * @returns Boolean if message is signed */ export function isSigned(message) { return Signature.is(message.signature); } /** * Requires a signed message and returns its signer address * * @param message - Signed message to retrieve signer address * @param caps - Sender's capabilities which may change how signature is verified * @returns Address which signed message */ export function getMessageSigner(message, caps) { // if !caps.immutableMetadata, partner doesn't sign the same thing they send, but the decoded // version of it (checksummed addresses) if (!getCap(caps, Capabilities.IMMUTABLE_METADATA) && 'metadata' in message) message = { ...message, metadata: decode(Metadata, message.metadata) }; return verifyMessage(arrayify(packMessage(message)), message.signature); } /** * Get the signed BalanceProof associated with an EnvelopeMessage * * @param message - Signed EnvelopeMessage * @returns Signed BalanceProof object for message */ export function getBalanceProofFromEnvelopeMessage(message) { return { chainId: message.chain_id, tokenNetworkAddress: message.token_network_address, channelId: message.channel_identifier, nonce: message.nonce, transferredAmount: message.transferred_amount, lockedAmount: message.locked_amount, locksroot: message.locksroot, additionalHash: createMessageHash(message), signature: message.signature, }; } /** * Encode a Message as a JSON string * Uses io-ts codec to encode BigNumbers as JSON 'string' type, as Raiden * * @param message - Message object to be serialized * @returns JSON string */ export function encodeJsonMessage(message) { if ('signature' in message) return jsonStringify(Signed(Message).encode(message)); return jsonStringify(Message.encode(message)); } /** * Try to decode text as a Message, using io-ts codec to decode BigNumbers * Throws if can't decode, or message is invalid regarding any of the encoded constraints * * @param text - JSON string to try to decode * @returns Message object */ export function decodeJsonMessage(text) { const parsed = jsonParse(text); assert(parsed && typeof parsed === 'object' && 'type' in parsed && Object.values(MessageType).some((t) => t === parsed['type']), `Invalid message type: ${parsed?.['type']}`); if ('signature' in parsed) return decode(Signed(Message), parsed); return decode(Message, parsed); } /** * Pack message and request signer to sign it, and returns signed message * * @param signer - Signer instance * @param message - Unsigned message to pack and sign * @param opts - Options * @param opts.log - Logger instance * @returns Promise to signed message */ export async function signMessage(signer, message, { log } = { log: logging }) { if (isSigned(message)) return message; log.debug(`Signing message "${message.type}"`, message); const signature = (await signer.signMessage(arrayify(packMessage(message)))); return { ...message, signature }; } /** * Typeguard to ensure an action is a messageReceived of any of a set of Message types * * @param messageCodecs - Message codec to test action.payload.message against * @returns Typeguard intersecting messageReceived action and payload.message schemas */ export function isMessageReceivedOfType(messageCodecs) { /** * Typeguard function * * @param action - Some action to guard to be a messageReceved * @returns Whether or not action is a messageReceved of given type */ return (action) => messageReceived.is(action) && (Array.isArray(messageCodecs) ? messageCodecs.some((c) => c.is(action.payload.message)) : messageCodecs.is(action.payload.message)); } /** * Parse a received message into either a Message or Signed<Message> * If Signed, the signer must match the sender's address. * Errors are logged and undefined returned * * @param line - String to be parsed as a single message * @param sender - Sender's presence * @param deps - Dependencies * @param deps.log - Logger instance * @returns Validated Signed or unsigned Message, or undefined */ export function parseMessage(line, sender, { log }) { if (typeof line !== 'string') return; try { const message = decodeJsonMessage(line); // if Signed, accept only if signature matches sender address if ('signature' in message) { const signer = getMessageSigner(message, sender.payload.caps); assert(signer === sender.meta.address, [ ErrorCodes.TRNS_MESSAGE_SIGNATURE_MISMATCH, { sender: sender.meta.address, signer }, ]); } return message; } catch (err) { log.warn(`Could not decode message: ${line}: ${err}`); } } //# sourceMappingURL=utils.js.map