import Web3 from 'web3' ; import ERC20ABI from '../contracts/abi/erc-20' ; import { AbiItem } from 'web3-utils' ; import { ContractOptions } from 'web3-eth-contract' ; import { gasEstimate , executeTransaction , getNextNonceFromEstimate , Operation , gasInToken } from './utils/safe-utils' ; import { signSafeTx } from './utils/signing-utils' ; import BN from 'bn.js' ; import { query } from './utils/graphql' ; import { TransactionReceipt } from 'web3-core' ; import { TransactionOptions , waitForSubgraphIndexWithTxnReceipt , isTransactionHash } from './utils/general-utils' ; const { fromWei } = Web3 . utils ; export interface ISafes { viewSafe ( safeAddress ) : Promise < ViewSafeResult > ; view ( options ? ) : Promise < ViewSafesResult > ; view ( owner ? ) : Promise < ViewSafesResult > ; view ( owner ? , options ? ) : Promise < ViewSafesResult > ; sendTokensGasEstimate ( safeAddress , tokenAddress , recipient , amount ) : Promise < string > ; sendTokens ( txnHash ) : Promise < TransactionReceipt > ; sendTokens ( safeAddress , tokenAddress , recipient , amount , txnOptions ? , contractOptions ? ) : Promise < TransactionReceipt > ; } export type Safe = DepotSafe | PrepaidCardSafe | MerchantSafe | RewardSafe | ExternalSafe ; interface BaseSafe { address : string ; createdAt : number ; tokens : TokenInfo [ ] ; owners : string [ ] ; } export interface DepotSafe extends BaseSafe { type : 'depot' ; infoDID ? : string ; } export interface MerchantSafe extends BaseSafe { type : 'merchant' ; accumulatedSpendValue : number ; merchant : string ; infoDID ? : string ; } export interface RewardSafe extends BaseSafe { type : 'reward' ; rewardProgramId : string ; } export interface ExternalSafe extends BaseSafe { type : 'external' ; } export interface PrepaidCardSafe extends BaseSafe { type : 'prepaid-card' ; issuingToken : string ; spendFaceValue : number ; prepaidCardOwner : string ; hasBeenUsed : boolean ; issuer : string ; reloadable : boolean ; transferrable : boolean ; customizationDID ? : string ; } export interface TokenInfo { tokenAddress : string ; token : { name : string ; symbol : string ; decimals : number ; } ; balance : string ; } export interface Options { viewAll : boolean ; type ? : Safe [ 'type' ] ; } const defaultOptions = { viewAll : false } ; export interface ViewSafeResult { safe : Safe | undefined ; blockNumber : number ; } export interface ViewSafesResult { safes : Safe [ ] ; blockNumber : number ; } const safeQueryFields = `
  id
  createdAt
  owners {
    owner {
      id
    }
  }
  tokens {
    balance
    token {
      id
      name
      symbol
    }
  }
  depot {
    id
    infoDid
  }
  prepaidCard {
    id
    customizationDID
    issuingToken {
      symbol
      id
    }
    faceValue
    payments {
      id
    }
    issuer {
      id
    }
    owner {
      id
    }
    reloadable
  }
  merchant {
    id
    spendBalance
    infoDid
    merchant {
      id
    }
  }
  reward {
    id
    rewardee {
      id
    }
  }
` ; const safeQuery = `
  query ($id: ID!) {
    safe(id: $id) {
      ${ safeQueryFields }
    }
    _meta {
      block {
        number
      }
    }
  }
` ; const safesQuery = `
  query($account: ID!) {
    account(id: $account) {
      safes(orderBy:ownershipChangedAt orderDirection:desc) {
        safe {
          ${ safeQueryFields }
        }
      }
    }

    _meta {
      block {
        number
      }
    }
  }
` ; const safesFilteredQuery = `
  query($account: ID!, $type: String) {
    account(id: $account) {
      safes(orderBy:ownershipChangedAt orderDirection:desc, where: {type: $type}) {
        safe {
          ${ safeQueryFields }
        }
      }
    }

    _meta {
      block {
        number
      }
    }
  }
` ; export async function viewSafe ( network , safeAddress ) { let { data : { safe , _meta } , } = await query ( network , safeQuery , { id : safeAddress } ) ; return { safe : processSafeResult ( safe as GraphQLSafeResult ) , blockNumber : _meta . block . number , } ; } export default class Safes implements ISafes { constructor ( private layer2Web3 ) { } async viewSafe ( safeAddress ) { let { data : { safe , _meta } , } = await query ( this . layer2Web3 , safeQuery , { id : safeAddress } ) ; return { safe : processSafeResult ( safe as GraphQLSafeResult ) , blockNumber : _meta . block . number , } ; } async view ( options ? ) ; async view ( owner ? ) ; async view ( owner ? , options ? ) ; async view ( ownerOrOptions ? , options ? ) { let owner ; let _options ; if ( typeof ownerOrOptions === 'string' ) { owner = ownerOrOptions ; } else { owner = ( await this . layer2Web3 . eth . getAccounts ( ) ) [ 0 ] ; _options = { ... defaultOptions , ... ( ownerOrOptions ?? { } ) } ; } _options = { ... defaultOptions , ... ( options ?? _options ?? { } ) } ; let account , _meta ; if ( options ?. type ) { let type = options . type === 'external' ? null : options . type ; ( { data : { account , _meta } , } = await query ( this . layer2Web3 , safesFilteredQuery , { account : owner , type } ) ) ; } else { ( { data : { account , _meta } , } = await query ( this . layer2Web3 , safesQuery , { account : owner } ) ) ; } if ( account == null ) { return { safes : [ ] , blockNumber : _meta . block . number , } ; } let { safes } = account ; let result = [ ] ; for ( let { safe } of safes as { safe : GraphQLSafeResult ; } [ ] ) { let safeResult = processSafeResult ( safe ) ; if ( safeResult ) { if ( _options . viewAll ) { result . push ( safeResult ) ; } else if ( safeResult . type === 'prepaid-card' && safeResult . spendFaceValue > 0 ) { result . push ( safeResult ) ; } else if ( safeResult . type === 'merchant' || safeResult . type === 'depot' || safeResult . type === 'reward' ) { result . push ( safeResult ) ; } } } return { safes : result , blockNumber : _meta . block . number , } ; } async sendTokensGasEstimate ( safeAddress , tokenAddress , recipient , amount ) { let token = new this . layer2Web3 . eth . Contract ( ERC20ABI as AbiItem [ ] , tokenAddress ) ; let safeBalance = new BN ( await token . methods . balanceOf ( safeAddress ) . call ( ) ) ; if ( safeBalance . lt ( new BN ( amount ) ) ) { throw new Error ( `Safe does not have enough balance to transfer tokens. The token ${ tokenAddress } balance of safe ${ safeAddress } is ${ fromWei ( safeBalance . toString ( ) ) }, amount to transfer ${ fromWei ( amount ) }` ) ; } let payload = this . transferTokenPayload ( tokenAddress , recipient , amount ) ; let estimate = await gasEstimate ( this . layer2Web3 , safeAddress , tokenAddress , '0' , payload , Operation . CALL , tokenAddress ) ; return gasInToken ( estimate ) . toString ( ) ; } async sendTokens ( txnHash ) ; async sendTokens ( safeAddress , tokenAddress , recipient , amount , txnOptions ? , contractOptions ? ) ; async sendTokens ( safeAddressOrTxnHash , tokenAddress ? , recipient ? , amount ? , txnOptions ? , contractOptions ? ) { if ( isTransactionHash ( safeAddressOrTxnHash ) ) { let txnHash = safeAddressOrTxnHash ; return waitForSubgraphIndexWithTxnReceipt ( this . layer2Web3 , txnHash ) ; } let safeAddress = safeAddressOrTxnHash ; if ( ! tokenAddress ) { throw new Error ( 'tokenAddress must be specified' ) ; } if ( ! recipient ) { throw new Error ( 'recipient must be specified' ) ; } if ( ! amount ) { throw new Error ( 'amount must be specified' ) ; } let { nonce , onNonce , onTxnHash } = txnOptions ?? { } ; let from = contractOptions ?. from ?? ( await this . layer2Web3 . eth . getAccounts ( ) ) [ 0 ] ; let token = new this . layer2Web3 . eth . Contract ( ERC20ABI as AbiItem [ ] , tokenAddress ) ; let symbol = await token . methods . symbol ( ) . call ( ) ; let safeBalance = new BN ( await token . methods . balanceOf ( safeAddress ) . call ( ) ) ; if ( safeBalance . lt ( new BN ( amount ) ) ) { throw new Error ( `Safe does not have enough balance to transfer tokens. The token ${ tokenAddress } balance of safe ${ safeAddress } is ${ fromWei ( safeBalance . toString ( ) ) }, amount to transfer ${ fromWei ( amount ) }` ) ; } let payload = this . transferTokenPayload ( tokenAddress , recipient , amount ) ; let estimate = await gasEstimate ( this . layer2Web3 , safeAddress , tokenAddress , '0' , payload , Operation . CALL , tokenAddress ) ; let gasCost = gasInToken ( estimate ) ; if ( safeBalance . lt ( new BN ( amount ) . add ( gasCost ) ) ) { throw new Error ( `Safe does not have enough balance to pay for gas when transfer tokens. The token ${ tokenAddress } balance of safe ${ safeAddress } is ${ fromWei ( safeBalance . toString ( ) ) } ${ symbol }, amount to transfer ${ fromWei ( amount ) } ${ symbol }, the gas cost is ${ fromWei ( gasCost ) } ${ symbol }` ) ; } if ( nonce == null ) { nonce = getNextNonceFromEstimate ( estimate ) ; if ( typeof onNonce === 'function' ) { onNonce ( nonce ) ; } } let result = await executeTransaction ( this . layer2Web3 , safeAddress , tokenAddress , payload , Operation . CALL , estimate , nonce , await signSafeTx ( this . layer2Web3 , safeAddress , tokenAddress , payload , estimate , nonce , from ) ) ; let txnHash = result . ethereumTx . txHash ; if ( typeof onTxnHash === 'function' ) { await onTxnHash ( txnHash ) ; } return await waitForSubgraphIndexWithTxnReceipt ( this . layer2Web3 , txnHash ) ; } private transferTokenPayload ( tokenAddress , recipient , amount ) { let token = new this . layer2Web3 . eth . Contract ( ERC20ABI as AbiItem [ ] , tokenAddress ) ; return token . methods . transfer ( recipient , amount ) . encodeABI ( ) ; } } interface GraphQLSafeResult { id : string ; createdAt : string ; owners : { owner : { id : string ; } ; } [ ] ; tokens : { balance : string ; token : { id : string ; name : string ; symbol : string ; } ; } [ ] ; depot : { id : string ; infoDid : string | null ; } | null ; prepaidCard : { id : string ; customizationDID : string | null ; issuingToken : { symbol : string ; id : string ; } ; owner : { id : string ; } ; payments : { id : string ; } [ ] ; faceValue : string ; issuer : { id : string ; } ; reloadable : boolean ; } | null ; merchant : { id : string ; spendBalance : string ; infoDid : string | null ; merchant : { id : string ; } ; } ; reward : { id : string ; rewardProgramId : string ; rewardee : { id : string ; } ; } ; } function processSafeResult ( safe ) { if ( ! safe ) { return ; } let tokens = [ ] ; let createdAt = parseInt ( safe . createdAt ) ; for ( let tokenDetail of safe . tokens ) { tokens . push ( { tokenAddress : tokenDetail . token . id , balance : tokenDetail . balance , token : { name : tokenDetail . token . name , symbol : tokenDetail . token . symbol , decimals : 18 , } , } ) ; } let owners = [ ] ; for ( let ownerInfo of safe . owners ) { let { owner : { id : owner } , } = ownerInfo ; owners . push ( owner ) ; } if ( safe . depot ) { let depot = { type : 'depot' , address : safe . depot . id , infoDID : safe . depot . infoDid ? safe . depot . infoDid : undefined , tokens , createdAt , owners , } ; return depot ; } else if ( safe . merchant ) { let merchant = { type : 'merchant' , address : safe . merchant . id , infoDID : safe . merchant . infoDid ? safe . merchant . infoDid : undefined , accumulatedSpendValue : parseInt ( safe . merchant . spendBalance ) , merchant : safe . merchant . merchant . id , tokens , createdAt , owners , } ; return merchant ; } else if ( safe . prepaidCard ) { let prepaidCard = { type : 'prepaid-card' , address : safe . prepaidCard . id , customizationDID : safe . prepaidCard . customizationDID ? safe . prepaidCard . customizationDID : undefined , issuingToken : safe . prepaidCard . issuingToken . id , spendFaceValue : parseInt ( safe . prepaidCard . faceValue ) , issuer : safe . prepaidCard . issuer . id , hasBeenUsed : safe . prepaidCard . payments . length > 0 , reloadable : safe . prepaidCard . reloadable , transferrable : safe . prepaidCard . payments . length === 0 && safe . prepaidCard . issuer . id === safe . prepaidCard . owner . id , prepaidCardOwner : safe . prepaidCard . owner . id , tokens , createdAt , owners , } ; return prepaidCard ; } else if ( safe . reward ) { let reward = { type : 'reward' , address : safe . reward . id , rewardProgramId : safe . reward . rewardProgramId , tokens , createdAt , owners , } ; return reward ; } else { let externalSafe = { type : 'external' , address : safe . id , tokens , createdAt , owners , } ; return externalSafe ; } }