import BN from 'bn.js' ; import Web3 from 'web3' ; import { TransactionReceipt , TransactionConfig } from 'web3-core' ; import { ContractOptions } from 'web3-eth-contract' ; import { AbiItem } from 'web3-utils' ; import ERC20ABI from '../contracts/abi/erc-20' ; import ForeignAMBABI from '../contracts/abi/foreign-amb' ; import ForeignBridgeABI from '../contracts/abi/foreign-bridge-mediator' ; import { getAddress } from '../contracts/addresses' ; import { TransactionOptions , waitUntilTransactionMined , isTransactionHash , waitUntilBlock , } from './utils/general-utils' ; export interface ITokenBridgeForeignSide { unlockTokens ( txnHash ) : Promise < TransactionReceipt > ; unlockTokens ( tokenAddress , amount , txnOptions ? , contractOptions ? ) : Promise < TransactionReceipt > ; relayTokens ( txnHash ) : Promise < TransactionReceipt > ; relayTokens ( tokenAddress , recipientAddress , amount , txnOptions ? , contractOptions ? ) : Promise < TransactionReceipt > ; claimBridgedTokens ( txnHash ) : Promise < TransactionReceipt > ; claimBridgedTokens ( messageId , encodedData , signatures , txnOptions ? , contractOptions ? ) : Promise < TransactionReceipt > ; } const CLAIM_BRIDGED_TOKENS_GAS_LIMIT = 350000 ; export default class TokenBridgeForeignSide implements ITokenBridgeForeignSide { constructor ( private layer1Web3 ) { } async unlockTokens ( txnHash ) ; async unlockTokens ( tokenAddress , amount , txnOptions ? , contractOptions ? ) ; async unlockTokens ( tokenAddressOrTxnHash , amount ? , txnOptions ? , contractOptions ? ) { if ( isTransactionHash ( tokenAddressOrTxnHash ) ) { let txnHash = tokenAddressOrTxnHash ; return await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ; } let tokenAddress = tokenAddressOrTxnHash ; if ( ! amount ) { throw new Error ( 'amount is required' ) ; } let from = contractOptions ?. from ?? ( await this . layer1Web3 . eth . getAccounts ( ) ) [ 0 ] ; let token = new this . layer1Web3 . eth . Contract ( ERC20ABI as AbiItem [ ] , tokenAddress ) ; let foreignBridge = await getAddress ( 'foreignBridge' , this . layer1Web3 ) ; let nextNonce = await this . getNextNonce ( from ) ; return await new Promise ( ( resolve , reject ) => { let { nonce , onNonce , onTxnHash } = txnOptions ?? { } ; let data = token . methods . approve ( foreignBridge , amount ) . encodeABI ( ) ; let tx = { ... contractOptions , from , to : tokenAddress , data , } ; if ( nonce != null ) { tx . nonce = parseInt ( nonce . toString ( ) ) ; } else if ( typeof onNonce === 'function' ) { onNonce ( nextNonce ) ; } this . layer1Web3 . eth . sendTransaction ( tx ) . on ( 'transactionHash' , async ( txnHash ) => { if ( typeof onTxnHash === 'function' ) { onTxnHash ( txnHash ) ; } try { resolve ( await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ) ; } catch ( e ) { reject ( e ) ; } } ) . on ( 'error' , ( error ) => { reject ( error ) ; } ) ; } ) ; } async relayTokens ( txnHash ) ; async relayTokens ( tokenAddress , recipientAddress , amount , txnOptions ? , contractOptions ? ) ; async relayTokens ( tokenAddressOrTxnHash , recipientAddress ? , amount ? , txnOptions ? , contractOptions ? ) { if ( isTransactionHash ( tokenAddressOrTxnHash ) ) { let txnHash = tokenAddressOrTxnHash ; return await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ; } let tokenAddress = tokenAddressOrTxnHash ; if ( ! recipientAddress ) { throw new Error ( 'recipientAddress is required' ) ; } if ( ! amount ) { throw new Error ( 'amount is required' ) ; } let from = contractOptions ?. from ?? ( await this . layer1Web3 . eth . getAccounts ( ) ) [ 0 ] ; let foreignBridgeAddress = await getAddress ( 'foreignBridge' , this . layer1Web3 ) ; let foreignBridge = new this . layer1Web3 . eth . Contract ( ForeignBridgeABI as any , foreignBridgeAddress ) ; let nextNonce = await this . getNextNonce ( from ) ; return await new Promise ( ( resolve , reject ) => { let { nonce , onNonce , onTxnHash } = txnOptions ?? { } ; let data = foreignBridge . methods . relayTokens ( tokenAddress , recipientAddress , amount ) . encodeABI ( ) ; let tx = { ... contractOptions , from , to : foreignBridgeAddress , data , } ; if ( nonce != null ) { tx . nonce = parseInt ( nonce . toString ( ) ) ; } else if ( typeof onNonce === 'function' ) { onNonce ( nextNonce ) ; } this . layer1Web3 . eth . sendTransaction ( tx ) . on ( 'transactionHash' , async ( txnHash ) => { if ( typeof onTxnHash === 'function' ) { onTxnHash ( txnHash ) ; } try { resolve ( await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ) ; } catch ( e ) { reject ( e ) ; } } ) . on ( 'error' , ( error ) => { reject ( error ) ; } ) ; } ) ; } async getEstimatedGasForWithdrawalClaim ( _tokenAddress ) { let withdrawalGasLimit = new BN ( CLAIM_BRIDGED_TOKENS_GAS_LIMIT ) ; let gasPrice = await this . layer1Web3 . eth . getGasPrice ( ) ; let estimatedGasInWei = withdrawalGasLimit . mul ( new BN ( gasPrice ) ) ; let rounder = new BN ( 1e12 ) ; return estimatedGasInWei . divRound ( rounder ) . mul ( rounder ) ; } async claimBridgedTokens ( txnHash ) ; async claimBridgedTokens ( messageId , encodedData , signatures , txnOptions ? , contractOptions ? ) ; async claimBridgedTokens ( messageIdOrTxnHash , encodedData ? , signatures ? , txnOptions ? , contractOptions ? ) { if ( ! encodedData ) { let txnHash = messageIdOrTxnHash ; return await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ; } let messageId = messageIdOrTxnHash ; if ( ! signatures ) { throw new Error ( 'signatures is required' ) ; } let from = contractOptions ?. from ?? ( await this . layer1Web3 . eth . getAccounts ( ) ) [ 0 ] ; let foreignAmbAddress = await getAddress ( 'foreignAMB' , this . layer1Web3 ) ; let foreignAmb = new this . layer1Web3 . eth . Contract ( ForeignAMBABI as AbiItem [ ] , foreignAmbAddress ) ; let events = await foreignAmb . getPastEvents ( 'RelayedMessage' , { fromBlock : 0 , toBlock : 'latest' , filter : { messageId } , } ) ; if ( events . length === 1 ) { let txnHash = events [ 0 ] . transactionHash ; return await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ; } const packedSignatures = prepSignaturesForExecution ( signatures ) ; let nextNonce = await this . getNextNonce ( from ) ; let { nonce , onNonce , onTxnHash } = txnOptions ?? { } ; return await new Promise ( ( resolve , reject ) => { let data = foreignAmb . methods . executeSignatures ( encodedData , packedSignatures ) . encodeABI ( ) ; let tx = { ... contractOptions , from , to : foreignAmbAddress , data , gas : CLAIM_BRIDGED_TOKENS_GAS_LIMIT , } ; if ( nonce != null ) { tx . nonce = parseInt ( nonce . toString ( ) ) ; } else if ( typeof onNonce === 'function' ) { onNonce ( nextNonce ) ; } this . layer1Web3 . eth . sendTransaction ( tx ) . on ( 'transactionHash' , async ( txnHash ) => { if ( typeof onTxnHash === 'function' ) { onTxnHash ( txnHash ) ; } try { resolve ( await waitUntilOneBlockAfterTxnMined ( this . layer1Web3 , txnHash ) ) ; } catch ( e ) { reject ( e ) ; } } ) . on ( 'error' , ( error ) => { reject ( error ) ; } ) ; } ) ; } private async getNextNonce ( from ? ) { from = from ?? ( await this . layer1Web3 . eth . getAccounts ( ) ) [ 0 ] ; let previousBlockNumber = ( await this . layer1Web3 . eth . getBlockNumber ( ) ) - 1 ; let nonce = await this . layer1Web3 . eth . getTransactionCount ( from , previousBlockNumber ) ; return new BN ( String ( nonce ) ) ; } } function prepSignaturesForExecution ( signatures ) { return packSignatures ( signatures . map ( signatureToVRS ) ) ; } function signatureToVRS ( rawSignature ) { const signature = strip0x ( rawSignature ) ; const v = signature . substr ( 64 * 2 ) ; const r = signature . substr ( 0 , 32 * 2 ) ; const s = signature . substr ( 32 * 2 , 32 * 2 ) ; return { v , r , s } ; } function packSignatures ( array ) { const length = strip0x ( Web3 . utils . toHex ( array . length ) ) ; const msgLength = length . length === 1 ? `0${ length }` : length ; const [ v , r , s ] = array . reduce ( ( [ vs , rs , ss ] , { v , r , s } ) => [ vs + v , rs + r , ss + s ] , [ '' , '' , '' ] ) ; return `0x${ msgLength }${ v }${ r }${ s }` ; } function strip0x ( s ) { return Web3 . utils . isHexStrict ( s ) ? s . substr ( 2 ) : s ; } async function waitUntilOneBlockAfterTxnMined ( web3 , txnHash ) { let receipt = await waitUntilTransactionMined ( web3 , txnHash ) ; await waitUntilBlock ( web3 , receipt . blockNumber + 1 ) ; return receipt ; }