import BigNumber from 'bignumber.js' ; import { Secp256k1 } from 'bitcoin-ts' ; import { randomBytes } from 'crypto' ; import { Contract , ethers } from 'ethers' ; import { promisify } from 'util' ; import UNIDIRECTIONAL_MAINNET from '../abi/Unidirectional-mainnet.json' ; import UNIDIRECTIONAL_TESTNET from '../abi/Unidirectional-testnet.json' ; import TOKEN_UNIDIRECTIONAL from '../abi/TokenUnidirectional.json' ; import { ContractReceipt } from 'ethers/contract' ; BigNumber . config ( { EXPONENTIAL_AT : 1e9 } ) ; const UNIDIRECTIONAL_METADATA = { /** Mainnet */ 1 : { metadata : UNIDIRECTIONAL_MAINNET , address : '0x08e4f70109ccc5135f50cc359d24cb7686247df4' } , /** Ropsten (cross-client PoW) */ 3 : { metadata : UNIDIRECTIONAL_TESTNET , address : '0x8ffdea290f4dcdc553841a432e56aa26c91ab777' } , /** Rinkeby (Geth PoA) */ 4 : { metadata : UNIDIRECTIONAL_TESTNET , address : '0x71ed284ea8e26e14b8f2d9b98ce4eff5a1f25120' } , /** Kovan (Parity PoA) */ 42 : { metadata : UNIDIRECTIONAL_TESTNET , address : '0x481029bf134710832f5b9debdd10275fb7816f59' } } ; const TOKEN_UNIDIRECTIONAL_METADATA = { /** Mainnet */ 1 : { metadata : TOKEN_UNIDIRECTIONAL , address : '0x59b941b53403f84f42d5c11117b35564881b72f6' } , /** Ropsten (cross-client PoW) */ 3 : { metadata : TOKEN_UNIDIRECTIONAL , address : '0xf55cf03626dc6d6fdd9e97f88aace0b2ecae34c1' } , /** Rinkeby (Geth PoA) */ 4 : { metadata : TOKEN_UNIDIRECTIONAL , address : '0x7660f4fb856c0dcf07439d93ec3fe3f438960b89' } , /** Kovan (Parity PoA) */ 42 : { metadata : TOKEN_UNIDIRECTIONAL , address : '0x396317f2ea46a1cea58a58d44d2d902f1a257588' } } ; export interface PaymentChannel { /** UNIX timestamp in milliseconds when channel state was last fetched */ lastUpdated : number ; /** Unique identifier on contract for this specific channel */ channelId : string ; /** Ethereum address of the receiver in the channel */ receiver : string ; /** Ethereum address of the sender in the channel */ sender : string ; /** Total collateral the sender added to the channel */ value : BigNumber ; /**
     * Number of blocks between the beginning of the dispute period and when
     * the sender could sweep the channel, if the receiver did not claim it
     */ disputePeriod : BigNumber ; /**
     * Block number when the sender can end the dispute to get all their money back
     * - Only defined if a dispute period is active
     */ disputedUntil ? : BigNumber ; /** Ethereum address of the contract the channel is created on */ contractAddress : string ; /**
     * Address of the ERC-20 token contract to use for the token in the payment channel
     * - Only defined if this uses the TokenUnidirectional contract for ERC-20s
     */ tokenContract ? : string ; /**
     * Value of the claim/amount that can be claimed
     * - If no claim signature is included, the value defaults to 0
     */ spent : BigNumber ; /** Valid signature to claim the channel */ signature ? : string ; } export interface ClaimablePaymentChannel extends PaymentChannel { /** Valid signature to claim the channel */ signature : string ; } export interface SerializedClaim { contractAddress : string ; channelId : string ; signature : string ; value : string ; tokenContract ? : string ; } export interface SerializedPaymentChannel { lastUpdated : number ; channelId : string ; receiver : string ; sender : string ; value : string ; disputePeriod : string ; disputedUntil ? : string ; contractAddress : string ; tokenContract ? : string ; spent : string ; signature ? : string ; } export interface SerializedClaimablePaymentChannel extends SerializedPaymentChannel { signature : string ; } param channel export const deserializePaymentChannel = ( channel ) : PaymentChannel => ( { ... channel , value : new BigNumber ( channel . value ) , disputePeriod : new BigNumber ( channel . disputePeriod ) , disputedUntil : typeof channel . disputedUntil === 'string' ? new BigNumber ( channel . disputedUntil ) : channel . disputedUntil , spent : new BigNumber ( channel . spent ) } ) ; /** Generate a pseudorandom hex string to use as a channel ID */ export const generateChannelId = async ( ) => '0x' + ( await promisify ( randomBytes ) ( 32 ) ) . toString ( 'hex' ) ; param signer param useTokenContract param contractAddress export const getContract = async ( signer , useTokenContract = false , contractAddress ? ) => { let contractMetadata ; if ( signer . provider ) { const { chainId } = await signer . provider . getNetwork ( ) ; contractMetadata = useTokenContract ? TOKEN_UNIDIRECTIONAL_METADATA [ chainId ] : UNIDIRECTIONAL_METADATA [ chainId ] ; } if ( ! contractMetadata ) { if ( contractAddress ) { contractMetadata = useTokenContract ? { metadata : TOKEN_UNIDIRECTIONAL , address : contractAddress } : { metadata : UNIDIRECTIONAL_TESTNET , address : contractAddress } ; } else { throw new Error ( `Machinomy is not supported on the current Ethereum chain` ) ; } } return new ethers . Contract ( contractMetadata . address , contractMetadata . metadata . abi , signer ) ; } ; /**
 * Check if a channel may have closed between two an initial state and a later proposed state
 * - Attempts to protect against channelId reuse in Machinomy contracts,
 *   in case a channel was closed and reopened
 */ export const didChannelClose = ( cachedChannel , updatedChannel ) => cachedChannel . channelId !== updatedChannel . channelId || cachedChannel . contractAddress . toLowerCase ( ) !== updatedChannel . contractAddress . toLowerCase ( ) || ! cachedChannel . disputePeriod . isEqualTo ( updatedChannel . disputePeriod ) || ( cachedChannel . disputedUntil && ( ! updatedChannel . disputedUntil || ! cachedChannel . disputedUntil . isEqualTo ( updatedChannel . disputedUntil ) ) ) || cachedChannel . receiver . toLowerCase ( ) !== updatedChannel . receiver . toLowerCase ( ) || cachedChannel . sender . toLowerCase ( ) !== updatedChannel . sender . toLowerCase ( ) || ( cachedChannel . tokenContract && ( ! updatedChannel . tokenContract || cachedChannel . tokenContract . toLowerCase ( ) !== updatedChannel . tokenContract . toLowerCase ( ) ) ) || updatedChannel . value . isLessThan ( cachedChannel . value ) ; param contract param cachedChannel export const updateChannel = async < TPaymentChannel extends PaymentChannel > ( contract , cachedChannel ) : Promise < TPaymentChannel | undefined > => fetchChannelById ( contract , cachedChannel . channelId ) . then ( updatedChannel => updatedChannel && ! didChannelClose ( cachedChannel , updatedChannel ) ? ( { ... updatedChannel , spent : cachedChannel . spent , signature : cachedChannel . signature } as TPaymentChannel ) : undefined ) . catch ( ( ) => cachedChannel ) ; param contract param channelId export const fetchChannelById = async ( contract , channelId ) : Promise < PaymentChannel | undefined > => { let { sender , receiver , settlingUntil , settlingPeriod , value , tokenContract } = await contract . functions . channels ( channelId ) ; if ( sender === ethers . constants . AddressZero ) { return ; } const disputedUntil = settlingUntil . gt ( 0 ) ? new BigNumber ( settlingUntil . toString ( ) ) : undefined ; return { lastUpdated : Date . now ( ) , contractAddress : contract . address , channelId , receiver , sender , disputedUntil , disputePeriod : new BigNumber ( settlingPeriod . toString ( ) ) , value : new BigNumber ( value . toString ( ) ) , spent : new BigNumber ( 0 ) , tokenContract } ; } ; export const prepareTransaction = async ( { methodName , params , contract , gasPrice , gasLimit , value = 0 } ) : Promise < { txFee : BigNumber ; sendTransaction : ( ) => Promise < ContractReceipt > ; } > => { const overrides = { value : ethers . utils . bigNumberify ( value . toString ( ) ) , gasPrice : ethers . utils . bigNumberify ( gasPrice . toString ( ) ) } ; const estimatedGasLimit = gasLimit ? ethers . utils . bigNumberify ( gasLimit . toString ( ) ) : await contract . estimate [ methodName ] ( ... params , overrides ) ; const txFee = new BigNumber ( gasPrice ) . times ( estimatedGasLimit . toString ( ) ) ; return { txFee , sendTransaction : async ( ) => { const tx = await contract . functions [ methodName ] ( ... params , { ... overrides , gasLimit : estimatedGasLimit } ) ; const receipt = await tx . wait ( 1 ) ; /**
             * Per EIP 658, a receipt of 1 indicates the tx was successful:
             * https://github.com/Arachnid/EIPs/blob/d1ae915d079293480bd6abb0187976c230d57903/EIPS/eip-658.md
             */ if ( receipt . status !== 1 ) { throw new Error ( 'Ethereum transaction reverted by the EVM' ) ; } return receipt ; } } ; } ; param receipt param eventName export const hasEvent = ( receipt , eventName ) : boolean => ! receipt . events || receipt . events . map ( o => o . event ) . includes ( eventName ) ; param channel export const hasClaim = ( channel ? ) : channel is ClaimablePaymentChannel => ! ! channel && ! ! channel . signature ; param channel export const spentFromChannel = ( channel ? ) : BigNumber => channel ? channel . spent : new BigNumber ( 0 ) ; param channel export const remainingInChannel = ( channel ? ) : BigNumber => channel ? channel . value . minus ( channel . spent ) : new BigNumber ( 0 ) ; param channel export const isDisputed = ( channel ) : boolean => ! ! channel . disputedUntil ; param secp256k1 param claim param recoveryAddress export const isValidClaimSignature = ( secp256k1 , claim , recoveryAddress ) : boolean => { const signature = claim . signature . slice ( 0 , - 2 ) ; const signatureBuffer = hexToBuffer ( signature ) ; const v = claim . signature . slice ( - 2 ) ; const recoveryId = v === '1c' ? 1 : 0 ; let publicKey ; try { publicKey = secp256k1 . recoverPublicKeyUncompressed ( signatureBuffer , recoveryId , createPaymentDigest ( claim . contractAddress , claim . channelId , claim . value , claim . tokenContract ) ) ; } catch ( err ) { return false ; } const senderAddress = ethers . utils . computeAddress ( publicKey ) ; return senderAddress . toLowerCase ( ) === recoveryAddress . toLowerCase ( ) ; } ; param contractAddress param channelId param value param tokenContract export const createPaymentDigest = ( contractAddress , channelId , value , tokenContract ? ) : Buffer => { const paramTypes = [ 'address' , 'bytes32' , 'uint256' ] ; const paramValues = [ contractAddress , channelId , value ] ; if ( tokenContract ) { paramTypes . push ( 'address' ) ; paramValues . push ( tokenContract ) ; } const paymentDigest = ethers . utils . solidityKeccak256 ( paramTypes , paramValues ) ; const paymentDigestBuffer = hexToBuffer ( paymentDigest ) ; const prefixedPaymentDigest = ethers . utils . hashMessage ( paymentDigestBuffer ) ; return hexToBuffer ( prefixedPaymentDigest ) ; } ; param hexStr export const hexToBuffer = ( hexStr ) => Buffer . from ( stripHexPrefix ( hexStr ) , 'hex' ) ; param hexStr export const stripHexPrefix = ( hexStr ) => hexStr . startsWith ( '0x' ) ? hexStr . slice ( 2 ) : hexStr ;