const get = require('lodash/get');
const set = require('lodash/set');
const omit = require('lodash/omit');
const cloneDeep = require('lodash/cloneDeep');
const camelCase = require('lodash/camelCase');
const { isValid } = require('@arcblock/did');
const ocapProto = require('@ocap/proto');
const { toTxHash } = require('@ocap/mcrypto');
const { createMessage, getMessageType, toTypeUrl } = require('@ocap/message');
const { bytesToHex, toBase58, toBase64, toHex, toBuffer, fromTokenToUnit, fromUnitToToken } = require('@ocap/util');
const { DEFAULT_TOKEN_DECIMAL } = require('@ocap/util/lib/constant');
const { formatFactoryState, preMintFromFactory, isValidFactory } = require('@ocap/asset');
const {
  toDelegateAddress,
  toAssetAddress,
  toFactoryAddress,
  toTokenAddress,
  toTokenFactoryAddress,
  toStakeAddress,
  toRollupAddress
} = require('@arcblock/did-util');

const debug = require('debug')(require('../package.json').name);

const { transactions, typeUrls, multiSignTxs, multiSignV2Txs } = ocapProto;

// FIXME: delete this after @ocap/proto updated
const filteredTransactions = transactions.filter(x => !['DeclareTx'].includes(x));

/**
 * Generate extension methods on the fly
 *
 * @public
 * @param {GraphQLClient} client
 */
const createExtensionMethods = client => {
  Object.defineProperty(client, 'ctx', {
    value: {
      loading: false,
      ready: false,
      callbacks: []
    }
  });

  /**
   * Format big number presentation amount to token number
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#fromUnitToToken
   * @param {string} value
   * @returns {string}
   */
  client.fromUnitToToken = async value => {
    const { token } = await client.getContext();
    return fromUnitToToken(value, token.decimal);
  };

  /**
   * Encode amount to corresponding token big number presentation
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#fromTokenToUnit
   * @param {number} amount
   * @returns {BN}
   */
  client.fromTokenToUnit = async amount => {
    const { token } = await client.getContext();
    return fromTokenToUnit(amount, token.decimal);
  };

  /**
   * List all transaction send methods
   * Each method can send one kind of transactions supported by forge core, such as `DeclareTx`
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#getTxSendMethods
   * @returns {Array<string>} method name list
   */
  client.getTxSendMethods = () => filteredTransactions.map(x => camelCase(`send_${x}`));

  /**
   * List all transaction encode methods, each method can be used to encode transaction to buffer and object
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#getTxEncodeMethods
   * @returns {Array<string>} method name list
   */
  client.getTxEncodeMethods = () => filteredTransactions.map(x => camelCase(`encode_${x}`));

  /**
   * List all transaction sign methods, each method can be used to sign transaction to an object
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#getTxSignMethods
   * @returns {Array<string>} method name list
   */
  client.getTxSignMethods = () => filteredTransactions.map(x => camelCase(`sign_${x}`));

  /**
   * List all transaction multi sign methods, each method can be used to do multi sign a transaction
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#getTxMultiSignMethods
   * @returns {Array<string>} method name list
   */
  client.getTxMultiSignMethods = () => multiSignTxs.concat(multiSignV2Txs).map(x => camelCase(`multi_sign_${x}`));

  /**
   * Get protobuf message class by name, only supports forge-built-in types
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#getType
   * @param {string} x
   * @returns {class|null} message type
   */
  client.getType = x => getMessageType(x).fn;

  /**
   * Decode transaction buffer/base64/base58 to an object
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#decodeTx
   * @param {buffer|hex|base48|base64} input
   * @returns {object} transaction object
   */
  client.decodeTx = input => {
    const Transaction = client.getType('Transaction');
    return Transaction.deserializeBinary(toBuffer(input)).toObject();
  };

  /**
   * Ensure a connection is bootstrapped with some meta info fetched from chain node
   *
   * @private
   * @param {string} [conn=undefined]
   * @returns {object}
   */
  client.getContext = async () => {
    if (client.ctx.ready) {
      return client.context;
    }

    if (client.ctx.loading) {
      return new Promise((resolve, reject) => {
        client.ctx.callbacks.push(err => {
          if (err) {
            reject(err);
          } else {
            resolve(client.context);
          }
        });
      });
    }

    client.ctx.loading = true;

    await new Promise(resolve => setTimeout(resolve, 0));

    let result;
    try {
      // Cannot combine these two requests into one, as it may cause socket hang up errors in certain environments (like GitHub CI)
      const [chainInfoResult, forgeStateResult] = await Promise.all([client.doRawQuery(`{
          getChainInfo {
            code
            info {
              network
              consensusVersion
            }
          }
        }`), client.doRawQuery(`{
          getForgeState {
            code
            state {
              token {
                address
                decimal
                symbol
              }
              vaults {
                slashedStake
                txFee
                txGas
              }
              txConfig {
                txFee {
                  fee
                  typeUrl
                }
                txGas {
                  price
                  createState
                  updateState
                  dataStorage
                  minStake
                  maxStake
                  stakeLockPeriod
                }
              }
            }
          }
        }`)]);
      result = {
        getChainInfo: chainInfoResult.getChainInfo,
        getForgeState: forgeStateResult.getForgeState
      };
    } catch (err) {
      client.ctx.loading = true;
      client.ctx.callbacks.forEach(cb => cb(err));
      throw err;
    }

    client.context = {
      chainId: get(result, 'getChainInfo.info.network'),
      consensus: get(result, 'getChainInfo.info.consensusVersion'),
      token: get(result, 'getForgeState.state.token'),
      txConfig: get(result, 'getForgeState.state.txConfig'),
      vaults: get(result, 'getForgeState.state.vaults'),
      tokenStateMap: new Map()
    };

    client.ctx.ready = true;
    client.ctx.loading = false;

    client.ctx.callbacks.forEach(cb => cb());

    debug('context ready');

    return client.context;
  };

  client.getTokenStateMap = async addressList => {
    const result = new Map();
    const notExistAddressList = [];
    addressList.forEach(address => {
      if (client.context.tokenStateMap.has(address)) {
        result.set(address, client.context.tokenStateMap.get(address));
      } else {
        notExistAddressList.push(address);
      }
    });

    if (result.size === addressList.length) {
      return result;
    }

    const getTokenState = async address => {
      const {
        getTokenState: { code, state }
      } = await client.doRawQuery(`{
        getTokenState(address: "${address}") {
          code
          state {
            decimal
          }
        }
      }`);

      if (code !== 'OK') {
        throw new Error(`get token ${address} state failed`);
      }

      result.set(address, state);
      client.context.tokenStateMap.set(address, state);
    };

    await Promise.all(notExistAddressList.map(address => getTokenState(address)));

    return result;
  };

  // Unify a client wallet | forge managed wallet
  const getWallet = wallet => {
    if (!wallet) {
      return {
        address: '',
        publicKey: ''
      };
    }

    if (typeof wallet.toAddress === 'function') {
      return {
        address: wallet.address,
        publicKey: wallet.publicKey
      };
    }

    return {
      address: wallet.address,
      publicKey: wallet.pk || wallet.publicKey || ''
    };
  };

  // Generate transaction sign/send/encode functions on fly
  filteredTransactions.forEach(x => {
    /**
     * Encode a transaction
     *
     * @param {object} input
     * @param {object} input.tx - data of the transaction
     * @param {object} input.tx.itx - the actual transaction object
     * @param {object} [input.tx.from] - the sender address, can be derived from wallet
     * @param {object} [input.tx.nonce] - the tx nonce, defaults to Date.now if not set
     * @param {object} [input.tx.chainId] - the chainId
     * @param {object} [input.tx.signature] - the signature
     * @param {object} [input.tx.signatures] - the signature list
     * @param {object} input.wallet - the wallet used to sign the transaction
     * @param {object} input.delegator - the wallet address that delegated permissions to the `input.wallet` address
     * @returns Promise
     */
    const txEncodeFn = async ({ tx, wallet, delegator }) => {
      const w = getWallet(wallet);
      const context = await client.getContext();

      // Determine sender address
      const address = tx.from || w.address;
      const pk = tx.pk || w.publicKey;

      // Determine chainId & nonce, only attach new one when not exist
      const nonce = typeof tx.nonce === 'undefined' ? Date.now() : tx.nonce;
      let chainId = tx.chainId || '';
      if (!chainId) {
        chainId = context.chainId;
      }

      // Determine signatures for multi sig
      let signatures = [];
      if (Array.isArray(tx.signatures)) {
        signatures = tx.signatures;
      }
      if (Array.isArray(tx.signaturesList)) {
        signatures = tx.signaturesList;
      }

      // Determine itx
      let itx = null;
      if (tx.itx.typeUrl && tx.itx.value) {
        itx = tx.itx;
      } else {
        itx = { type: x, value: tx.itx };
      }

      const type = itx.typeUrl || toTypeUrl(itx.type);
      const feeConfig = get(context, 'txConfig.txFee', []).find(t => t.typeUrl === type);

      const txObj = createMessage('Transaction', {
        from: tx.delegator ? address : delegator || address,
        nonce,
        pk,
        chainId,
        signature: tx.signature || Buffer.from([]),
        signatures,
        delegator: tx.delegator || (delegator ? address : ''),
        itx,
        serviceFee: feeConfig ? feeConfig.fee : '0'
      });
      const txToSignBytes = txObj.serializeBinary();

      debug(`encodeTx.${x}.txObj`, txObj.toObject());

      return { object: txObj.toObject(), buffer: Buffer.from(txToSignBytes) };
    };

    const encodeMethod = camelCase(`encode_${x}`);
    txEncodeFn.__tx__ = encodeMethod;
    client[encodeMethod] = txEncodeFn;

    /**
     * Send a transaction
     *
     * @param {object} input
     * @param {object} input.tx - data of the transaction
     * @param {object} input.tx.itx - the actual transaction object
     * @param {object} [input.tx.from] - the sender address, can be derived from wallet
     * @param {object} [input.tx.nonce] - the tx nonce, defaults to Date.now if not set
     * @param {object} [input.tx.chainId] - the chainId
     * @param {object} [input.tx.signature] - the signature
     * @param {object} [input.tx.signatures] - the signatures
     * @param {object} [input.commit=false] - whether we should wait for transaction commit
     * @param {object} [input.extra] - extra params to verify the transaction, usually used for passkeys
     * @param {object} input.wallet - the wallet used to sign the transaction, either a forge managed wallet or user managed wallet
     * @param {object} [input.signature] - the signature of the tx, if exist, we will not sign the transaction
     * @param {object} input.delegator - the wallet address that delegated permissions to the `input.wallet` address
     * @returns {Promise<string>}
     */
    const txSendFn = async ({ tx, wallet, signature, delegator, commit = false, extra: txExtra = '' }, extra = {}) => {
      let encoded;
      if (signature) {
        encoded = tx;
        encoded.signature = signature;
      } else if (tx.signature) {
        const res = await txEncodeFn({ tx, wallet, delegator });
        encoded = res.object;
      } else {
        const res = await txEncodeFn({ tx, wallet, delegator });
        encoded = res.object;
        encoded.signature = await wallet.sign(bytesToHex(res.buffer));
      }

      if (extra['x-gas-payer-sig']) {
        console.error('ERROR: "x-gas-payer-sig" should only exist in extra.headers');
      }
      if (extra['x-gas-payer-pk']) {
        console.error('ERROR: "x-gas-payer-pk" should only exist in extra.headers');
      }

      return new Promise(async (resolve, reject) => {
        let txHash = null;
        try {
          const txMsg = createMessage('Transaction', encoded);
          const txBytes = txMsg.serializeBinary();
          const txStr = toBase64(txBytes);
          txHash = toTxHash(txBytes);
          debug(`sendTx.${x}.txObj`, txMsg.toObject());
          const { hash } = await client.sendTx({ tx: txStr, commit, extra: typeof txExtra === 'string' ? txExtra : JSON.stringify(txExtra) }, extra);
          resolve(hash);
        } catch (err) {
          err.txHash = txHash;
          reject(err);
        }
      });
    };

    const sendMethod = camelCase(`send_${x}`);
    txSendFn.__tx__ = sendMethod;
    client[sendMethod] = txSendFn;

    const _formatEncodedTx = async (tx, encoding) => {
      if (encoding) {
        const { buffer: txBytes } = await txEncodeFn({ tx });
        if (encoding === 'base64') {
          return toBase64(txBytes);
        }
        if (encoding === 'base58') {
          return toBase58(txBytes);
        }
        if (encoding === 'base16' || encoding === 'hex') {
          return toHex(txBytes);
        }
        return txBytes;
      }

      return tx;
    };

    // Generate transaction signing function
    const txSignFn = async ({ tx, wallet, delegator, encoding = '' }) => {
      if (tx.signature) {
        delete tx.signature;
      }

      const { object, buffer } = await txEncodeFn({ tx, wallet, delegator });
      object.signature = await wallet.sign(buffer);

      return _formatEncodedTx(object, encoding);
    };
    const signMethod = camelCase(`sign_${x}`);
    txSignFn.__tx__ = signMethod;
    client[signMethod] = txSignFn;

    // TODO: verify existing signatures before adding new signatures
    // Generate transaction multi sign function
    if (multiSignTxs.includes(x)) {
      const txMultiSignFn = async ({ tx, wallet, delegator, data, encoding = '' }) => {
        if (typeof wallet.toAddress !== 'function') {
          throw new Error('Multisig requires a valid wallet');
        }
        tx.signaturesList = tx.signatures || tx.signaturesList || [];
        if (delegator) {
          tx.signaturesList.unshift({
            pk: wallet.publicKey,
            signer: delegator,
            delegator: wallet.address,
            data
          });
        } else {
          tx.signaturesList.unshift({
            pk: wallet.publicKey,
            signer: wallet.address,
            delegator: '',
            data
          });
        }

        const { object, buffer } = await txEncodeFn({ tx, wallet });
        object.signaturesList[0].signature = await wallet.sign(bytesToHex(buffer));
        return _formatEncodedTx(object, encoding);
      };
      const multiSignMethod = camelCase(`multi_sign_${x}`);
      txMultiSignFn.__tx__ = multiSignMethod;
      client[multiSignMethod] = txMultiSignFn;
    }

    if (multiSignV2Txs.includes(x)) {
      const txMultiSignFn = async ({ tx, wallet, encoding = '' }) => {
        if (typeof wallet.toAddress !== 'function') {
          throw new Error('Multisig requires a valid wallet');
        }

        const signer = wallet.address;
        const signatures = cloneDeep(tx.signatures || tx.signaturesList || []);
        const item = signatures.find(s => s.signer === signer || s.delegator === signer);
        if (!item) {
          throw new Error('Signer not in the list');
        }

        tx.signaturesList = signatures.map(s => omit(s, ['signature']));
        const { object, buffer } = await txEncodeFn({ tx, wallet });

        item.signature = await wallet.sign(bytesToHex(buffer));
        object.signaturesList = signatures;
        return _formatEncodedTx(object, encoding);
      };
      const multiSignMethod = camelCase(`multi_sign_${x}`);
      txMultiSignFn.__tx__ = multiSignMethod;
      client[multiSignMethod] = txMultiSignFn;
    }
  });

  /**
   * Migrate current account to a new account
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#migrateAccount
   * @param {object} params
   * @param {WalletObject} params.from - which account to migrate from
   * @param {WalletObject} params.to - which account to migrate to
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the transaction hash once resolved
   */
  client.migrateAccount = ({ from, to }, extra) => client.sendAccountMigrateTx({
    tx: {
      itx: {
        address: to.address,
        pk: to.publicKey,
        type: to.type
      }
    },
    wallet: from
  }, extra);

  /**
   * Delegate some privileges to another account
   * So that that account can send transactions on behalf of the delegator
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#delegate
   * @param {object} params
   * @param {WalletObject} params.from - the delegator, who grants the privilege to others
   * @param {WalletObject} params.to - the delegatee, who is authorized to send transactions
   * @param {Array} params.privileges - the privilege settings
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `[transactionHash, delegateAddress]` once resolved
   */
  client.delegate = async ({ from, to, privileges }, extra) => {
    let ops = Array.isArray(privileges) ? privileges : [privileges];
    ops = ops.map(x => {
      if (x.typeUrl && x.limit) {
        return x;
      }

      return { typeUrl: x.typeUrl, limit: x.limit || { tokens: [], assets: [] } };
    });

    const txTypes = Object.values(typeUrls).filter(x => x.startsWith('fg:t:'));
    if (ops.some(x => txTypes.includes(x.typeUrl) === false)) {
      throw new Error('Invalid type url provided for delegation');
    }

    const address = toDelegateAddress(from.address, to.address);
    const hash = await client.sendDelegateTx({
      tx: {
        itx: {
          address,
          to: to.address,
          ops
        }
      },
      wallet: from
    }, extra);

    return [hash, address];
  };

  /**
   * Revoke a previous delegation
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#revokeDelegate
   * @param {object} params
   * @param {WalletObject} params.from - the delegator, who grants the privilege to others
   * @param {WalletObject} params.to - the delegatee, who is authorized to send transactions
   * @param {Array} params.privileges - the privilege settings
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the transaction hash once resolved
   */
  client.revokeDelegate = ({ from, to, privileges }, extra) => client.sendRevokeDelegateTx({
    tx: {
      itx: {
        address: toDelegateAddress(from.address, to.address),
        to: to.address,
        typeUrls: privileges.filter(Boolean).map(x => x.toString())
      }
    },
    wallet: from
  }, extra);

  /**
   * Create an new asset (non-fungible-token)
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#createAsset
   * @param {object} params
   * @param {string} params.moniker - asset name
   * @param {string} params.parent - asset parent
   * @param {object} params.data - asset data payload
   * @param {number} params.ttl - ttl after first consumption
   * @param {boolean} params.readonly - whether the asset can be updated after creation
   * @param {boolean} params.transferrable - whether the asset can be transferred to another account
   * @param {object} params.display - the display of the asset
   * @param {object} params.endpoint - the endpoint of the asset
   * @param {string} params.tags - the tag list of the asset
   * @param {string} params.delegator - who authorized this transaction
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `[transactionHash, assetAddress]` once resolved
   */
  client.createAsset = async ({
    moniker,
    parent = '',
    ttl = 0,
    data,
    readonly = false,
    transferrable = true,
    display,
    endpoint,
    tags = [],
    delegator = '',
    wallet
  }, extra) => {
    const payload = { moniker, parent, ttl, readonly, transferrable, data, display, endpoint, tags };
    const address = toAssetAddress(payload);
    payload.address = address;
    const hash = await client.sendCreateAssetTx({
      tx: { itx: payload },
      delegator,
      wallet
    }, extra);
    return [hash, address];
  };

  /**
   * Update an existing asset (non-fungible-token)
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#updateAsset
   * @param {object} params
   * @param {string} params.address - asset address
   * @param {string} params.moniker - asset name
   * @param {object} params.data - asset data payload
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.updateAsset = ({ address, moniker, data, wallet }, extra) => client.sendUpdateAssetTx({
    tx: {
      itx: {
        moniker,
        address,
        data
      }
    },
    wallet
  }, extra);

  /**
   * Create an asset factory that can be used to mint new assets in future transactions
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#createAssetFactory
   * @param {object} params
   * @param {string} params.factory - asset factory
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `[transactionHash, factoryAddress]` once resolved
   */
  client.createAssetFactory = async ({ factory, wallet }, extra) => {
    const itx = {
      name: factory.name,
      description: factory.description,
      settlement: factory.settlement || 'instant',
      limit: factory.limit || 0,
      trustedIssuers: factory.trustedIssuers || [],
      input: factory.input,
      output: factory.output,
      data: factory.data || null,
      hooks: factory.hooks || [],
      display: factory.display || null
    };

    itx.address = toFactoryAddress(itx);

    isValidFactory(itx, true);

    const hash = await client.sendCreateFactoryTx({ tx: { itx }, wallet }, extra);
    return [hash, itx.address];
  };

  /**
   * Prepare an acquire_asset itx
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#preMintAsset
   * @param {object} params
   * @param {string} params.factory - asset factory address
   * @param {object} params.inputs - factory inputs
   * @param {string} params.owner - who will own the asset
   * @param {WalletObject} params.wallet - the wallet of the asset issuer
   * @param {object} extra - other param to merge into the itx
   * @returns {Promise} the `[transactionHash, factoryAddress]` once resolved
   */
  client.preMintAsset = async ({ factory, inputs = {}, owner, wallet, extra = {} }) => {
    const options = { ignoreFields: ['context.genesisTx', 'context.renaissanceTx'] };
    const [{ state: factoryState }, { state: issuerState }] = await Promise.all([client.getFactoryState({ address: factory }, options), client.getAccountState({ address: wallet.address }, options)]);

    if (!factoryState) {
      throw new Error('Factory does not exist on chain');
    }

    if (!issuerState) {
      throw new Error('Issuer does not exist on chain');
    }

    const { asset, address, issuer, variables } = await preMintFromFactory({
      factory: formatFactoryState(factoryState),
      inputs,
      owner,
      issuer: { wallet, name: issuerState.moniker }
    });

    return Object.assign({}, extra, {
      asset: Object.assign({ address, owner, parent: factory }, asset),
      factory,
      address,
      variables: Object.entries(variables).map(([key, value]) => ({ name: key, value })),
      issuer,
      owner
    });
  };

  /**
   * Acquire an asset from factory
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#acquireAsset
   * @param {object} params
   * @param {AcquireAssetV2Tx} params.itx - result from preMintAsset
   * @param {string} params.delegator - who authorized this transaction
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.acquireAsset = ({ itx, delegator = '', wallet }, extra) => client.sendAcquireAssetV2Tx({ tx: { itx }, delegator, wallet }, extra);

  /**
   * Mint an asset from factory
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#mintAsset
   * @param {object} params
   * @param {MintAssetTx} params.itx - result from preMintAsset
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.mintAsset = ({ itx, wallet }, extra) => client.sendMintAssetTx({ tx: { itx }, wallet }, extra);

  /**
   * Transfer token or assets to another account
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#transfer
   * @param {object} params
   * @param {number} params.token - how much token can be transferred
   * @param {Array} params.assets - which assets should be transferred
   * @param {string} params.to - who receive the transfer
   * @param {string} params.memo - transaction note
   * @param {string} params.delegator - who authorized this transaction
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.transfer = async ({ token = 0, assets = [], tokens = [], to = '', memo = '', delegator = '', wallet }, extra) => {
    if (isValid(to) === false) {
      throw new Error('Can not transfer without valid recipient');
    }
    if (!token && tokens.length === 0 && assets.length === 0) {
      throw new Error('Can not transfer without payload');
    }

    const { token: contextToken } = await client.getContext();

    let tmpTokens = [];
    if (tokens.length > 0) {
      const tokensMap = await client.getTokenStateMap(tokens.map(t => t.address));
      tmpTokens = tokens.map(t => {
        t.value = fromTokenToUnit(t.value, tokensMap.get(t.address).decimal).toString();
        return t;
      });
    }

    return client.sendTransferV2Tx({
      tx: {
        itx: {
          to,
          value: fromTokenToUnit(token, contextToken.decimal),
          assets,
          tokens: tmpTokens,
          data: {
            typeUrl: 'json',
            value: memo || 'empty'
          }
        }
      },
      delegator,
      wallet
    }, extra);
  };

  /**
   * Stake token or assets for another account
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#stake
   * @param {object} params
   * @param {Array} params.assets - which assets to stake
   * @param {Array} params.tokens - which tokens to stake
   * @param {string} params.to - who receive the stake
   * @param {boolean} params.locked - is the stake locked on creation
   * @param {string[]} params.slashers - who can slash from this, default to [to]
   * @param {string} params.message - stake note
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` and `stakeAddress` once resolved
   */
  client.stake = async ({ assets = [], tokens = [], slashers = [], to = '', locked = false, message = '', nonce = '', wallet }, extra) => {
    let tmpTokens = [];
    if (tokens.length > 0) {
      const tokensMap = await client.getTokenStateMap(tokens.map(t => t.address));
      tmpTokens = tokens.map(t => {
        t.value = fromTokenToUnit(t.value, tokensMap.get(t.address).decimal).toString();
        return t;
      });
    }

    const from = wallet.address;
    const address = toStakeAddress(from, to, nonce);
    let tx = await client.multiSignStakeTx({
      tx: {
        itx: {
          address,
          receiver: to,
          locked,
          message,
          slashers,
          nonce,
          inputs: [{
            owner: from,
            assets,
            tokens: tmpTokens
          }]
        },
        signatures: [{
          signer: from,
          pk: wallet.publicKey
        }]
      },
      wallet
    }, extra);

    tx = await client.signStakeTx({ tx, wallet });
    const hash = await client.sendStakeTx({ tx, wallet }, extra);
    return [hash, address];
  };

  /**
   * Revoke token or assets from some stake
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#revokeStake
   * @param {object} params
   * @param {Array} params.assets - which assets to stake
   * @param {Array} params.tokens - which tokens to stake
   * @param {string} params.from - stake address
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.revokeStake = async ({ assets = [], tokens = [], from = '', wallet }, extra) => {
    let tmpTokens = [];
    if (tokens.length > 0) {
      const tokensMap = await client.getTokenStateMap(tokens.map(t => t.address));
      tmpTokens = tokens.map(t => {
        t.value = fromTokenToUnit(t.value, tokensMap.get(t.address).decimal).toString();
        return t;
      });
    }

    return client.sendRevokeStakeTx({
      tx: {
        itx: {
          address: from,
          outputs: [{
            owner: wallet.address,
            assets,
            tokens: tmpTokens
          }]
        }
      },
      wallet
    }, extra);
  };

  /**
   * Slash token or assets from some stake
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#slashStake
   * @param {object} params
   * @param {Array} params.assets - which assets to slash
   * @param {Array} params.tokens - which tokens to slash
   * @param {string} params.from - stake address
   * @param {string} params.reason - slash reason
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.slashStake = async ({ assets = [], tokens = [], reason, from, wallet }, extra) => {
    if (!reason) {
      throw new Error('Can not slash stake without valid reason message');
    }
    if (!from) {
      throw new Error('Can not slash stake without stake address');
    }

    const { vaults } = await client.getContext();

    let tmpTokens = [];
    if (tokens.length > 0) {
      const tokensMap = await client.getTokenStateMap(tokens.map(t => t.address));
      tmpTokens = tokens.map(t => {
        t.value = fromTokenToUnit(t.value, tokensMap.get(t.address).decimal).toString();
        return t;
      });
    }

    return client.sendSlashStakeTx({
      tx: {
        itx: {
          address: from,
          message: reason,
          outputs: [{
            owner: vaults.slashedStake,
            assets,
            tokens: tmpTokens
          }]
        }
      },
      wallet
    }, extra);
  };

  /**
   * Claim revoked token or assets from stake
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#claimStake
   * @param {object} params
   * @param {string} params.from - stake address
   * @param {Array} params.evidence - which revoke tx hash to use
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.claimStake = async ({ from, evidence, wallet }, extra) => {
    if (!evidence) {
      throw new Error('Can not claim stake without valid evidence tx hash');
    }
    if (!from) {
      throw new Error('Can not claim stake without stake address');
    }

    return client.sendClaimStakeTx({
      tx: {
        itx: {
          address: from,
          evidence: { hash: evidence }
        }
      },
      wallet
    }, extra);
  };

  /**
   * Create an rollup (sub-chain that lives in ocap)
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#createRollup
   * @param {object} params
   * @param {string} params.tokenAddress - ocap token address
   * @param {string} params.contractAddress - rollup contract address
   * @param {string} params.minStakeAmount
   * @param {string} params.maxStakeAmount
   * @param {number} params.minSignerCount
   * @param {number} params.maxSignerCount
   * @param {number} params.minBlockSize
   * @param {number} params.maxBlockSize
   * @param {number} params.minBlockInterval - in seconds
   * @param {number} params.minBlockConfirmation
   * @param {number} params.leaveWaitingPeriod
   * @param {number} params.publishWaitingPeriod
   * @param {number} params.publishSlashRate
   * @param {Array} params.seedValidators - list of seed validators
   * @param {object} params.data - data payload
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `[transactionHash, rollupAddress]` once resolved
   */
  client.createRollup = async (props, extra) => {
    const requiredProps = ['tokenAddress', 'vaultAddress', 'contractAddress', 'seedValidators', 'minStakeAmount'];
    for (const key of requiredProps) {
      if (!props[key]) {
        throw new Error(`Missing required property: ${key} when creating rollup`);
      }
    }

    const { state } = await client.getTokenState({ address: props.tokenAddress });
    if (!state) {
      throw new Error(`Token ${props.tokenAddress} not found when creating rollup`);
    }
    if (!state.foreignToken) {
      throw new Error(`Token ${props.tokenAddress} does not have foreignToken attached`);
    }

    const toBNStr = n => fromTokenToUnit(n, state.decimal).toString(10);
    const itx = Object.assign({
      maxStakeAmount: props.minStakeAmount,
      minSignerCount: 3,
      maxSignerCount: 5,
      minBlockSize: 1,
      maxBlockSize: 10,
      minBlockInterval: 1 * 60, // 1 minute
      minBlockConfirmation: 3,
      minDepositAmount: toBNStr(100),
      maxDepositAmount: toBNStr(1000000),
      minWithdrawAmount: toBNStr(100),
      maxWithdrawAmount: toBNStr(1000000),
      depositFeeRate: 100, // 1%
      withdrawFeeRate: 100, // 1%
      publisherFeeShare: 6000, // 60%
      proposerFeeShare: 3000, // 30%
      // validatorFeeShare: 1000, // 10%
      minDepositFee: toBNStr(1),
      maxDepositFee: toBNStr(10000),
      minWithdrawFee: toBNStr(1),
      maxWithdrawFee: toBNStr(10000),
      leaveWaitingPeriod: 60 * 60, // 1 hour
      publishWaitingPeriod: 60 * 60, // 1 hour
      publishSlashRate: 10 // 0.1%
    }, props);

    const address = toRollupAddress(itx);
    itx.address = address;

    const hash = await client.sendCreateRollupTx({ tx: { itx }, wallet: props.wallet }, extra);

    return [hash, address];
  };

  /**
   * Prepare an exchange transaction between multiple accounts
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#prepareExchange
   * @param {object} params
   * @param {number} params.offerToken - how much token can be sent
   * @param {Array} params.offerAssets - which assets should be sent
   * @param {number} params.demandToken - how much token can be received
   * @param {Array} params.demandAssets - which assets should be received
   * @param {string} params.receiver - who receive the transfer
   * @param {string} params.memo - transaction note
   * @param {string} params.delegator - which assets should be transferred
   * @param {WalletObject} params.wallet - the wallet who is the offerer
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transaction` object once resolved
   */
  client.prepareExchange = async ({
    offerToken = 0,
    offerAssets = [],
    demandToken = 0,
    demandAssets = [],
    offerTokens = [],
    demandTokens = [],
    receiver = '',
    memo = '',
    delegator = '',
    wallet
  }, extra) => {
    const { token } = await client.getContext();
    const tokensMap = await client.getTokenStateMap([...offerTokens, ...demandTokens].map(t => t.address));

    const tmpOfferTokens = offerTokens.map(t => {
      t.value = fromTokenToUnit(t.value, tokensMap.get(t.address).decimal).toString();
      return t;
    });
    const tmpDemandTokens = demandTokens.map(t => {
      t.value = fromTokenToUnit(t.value, tokensMap.get(t.address).decimal).toString();
      return t;
    });

    return client.signExchangeV2Tx({
      tx: {
        itx: {
          to: receiver,
          sender: {
            value: fromTokenToUnit(offerToken, token.decimal),
            assets: Array.isArray(offerAssets) ? offerAssets : [],
            tokens: tmpOfferTokens
          },
          receiver: {
            value: fromTokenToUnit(demandToken, token.decimal),
            assets: Array.isArray(demandAssets) ? demandAssets : [],
            tokens: tmpDemandTokens
          },
          data: {
            typeUrl: 'json',
            value: memo || 'empty'
          }
        }
      },
      delegator,
      wallet
    }, extra);
  };

  /**
   * Finalize an exchange transaction between multiple accounts
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#finalizeExchange
   * @param {object} params
   * @param {object} params.tx - the transaction object from `prepareExchange`
   * @param {string} params.delegator - who authorized this transaction
   * @param {object} params.data - extra data in the multi sig
   * @param {WalletObject} params.wallet - the wallet who is the demander
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transaction` object once resolved
   */
  client.finalizeExchange = ({ tx, delegator = '', data, wallet }, extra) => client.multiSignExchangeV2Tx({ tx, delegator, data, wallet }, extra);

  /**
   * Send an exchange transaction
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#exchange
   * @param {object} params
   * @param {object} params.tx - the transaction object from `finalizeExchange`
   * @param {WalletObject} params.wallet - the wallet to sign the transaction
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.exchange = ({ tx, wallet }, extra) => client.sendExchangeV2Tx({ tx, wallet }, extra);

  /**
   * Create an new token
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#createToken
   * @param {object} params
   * @param {string} params.name
   * @param {string} params.description
   * @param {string} params.symbol
   * @param {string} params.unit
   * @param {string} params.icon
   * @param {number} params.totalSupply
   * @param {number} params.initialSupply
   * @param {number} params.foreignToken
   * @param {object} params.data - asset data payload
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `[transactionHash, assetAddress]` once resolved
   */
  client.createToken = async ({
    name,
    description,
    symbol,
    unit,
    decimal,
    icon,
    totalSupply,
    initialSupply,
    maxTotalSupply,
    foreignToken,
    data,
    wallet
  }, extra) => {
    const tokenDecimal = typeof decimal === 'undefined' ? DEFAULT_TOKEN_DECIMAL : decimal;

    const payload = {
      name,
      description,
      symbol,
      unit,
      decimal: tokenDecimal,
      icon,
      totalSupply: fromTokenToUnit(totalSupply, tokenDecimal).toString(),
      initialSupply: fromTokenToUnit(initialSupply || totalSupply, tokenDecimal).toString(),
      maxTotalSupply: maxTotalSupply ? fromTokenToUnit(maxTotalSupply, tokenDecimal).toString() : null,
      foreignToken: foreignToken || null,
      data
    };

    const address = toTokenAddress(payload);
    payload.address = address;
    const hash = await client.sendCreateTokenTx({
      tx: { itx: payload },
      wallet
    }, extra);
    return [hash, address];
  };

  /**
   * Create an new token factory
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#createToken
   * @param {object} params
   * @param {CurveConfig} params.curve - curve config
   * @param {number} params.feeRate - fee for the mint and burn
   * @param {TokenState} params.token - token state payload
   * @param {object} params.data - token factory data payload
   * @param {WalletObject} params.wallet - the initial owner of the asset
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `[transactionHash, tokenFactoryAddress]` once resolved
   */
  client.createTokenFactory = async ({ feeRate = 0, curve, token, data, wallet }, extra) => {
    const context = await client.getContext();

    const payload = {
      feeRate,
      curve,
      reserveAddress: context.token.address,
      token,
      data
    };

    // format token value to unit
    const priceKeys = ['curve.slope', 'curve.basePrice', 'curve.fixedPrice'];
    for (const key of priceKeys) {
      const value = get(payload, key);
      if (value) {
        set(payload, key, fromTokenToUnit(value, context.token.decimal).toString());
      }
    }

    payload.token.maxTotalSupply = payload.token.maxTotalSupply ? fromTokenToUnit(payload.token.maxTotalSupply, payload.token.decimal).toString() : null;

    payload.token.address = toTokenAddress(payload.token);
    payload.address = toTokenFactoryAddress(payload);

    const hash = await client.sendCreateTokenFactoryTx({
      tx: { itx: payload },
      wallet
    }, extra);
    return [hash, payload.address];
  };

  /**
   * Mint token
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#mintToken
   * @param {object} params
   * @param {string} params.tokenFactory - token factory address
   * @param {number} params.amount - amount of token to mint
   * @param {string} params.receiver - receiver address
   * @param {string} params.maxReserve - max reserve cost of minting
   * @param {object} params.data - token factory data payload
   * @param {WalletObject} params.wallet - the initial owner of the token factory
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.mintToken = async ({ tokenFactory, amount, receiver, maxReserve, data, wallet }, extra) => {
    const { state: tokenFactoryState } = await client.getTokenFactoryState({ address: tokenFactory });

    const hasCurve = !!tokenFactoryState.curve;

    let tx = await client.multiSignMintTokenTx({
      tx: {
        itx: {
          tokenFactory,
          amount: fromTokenToUnit(amount, tokenFactoryState.token.decimal).toString(),
          receiver,
          data,
          inputs: hasCurve ? [{
            owner: wallet.address,
            tokens: [{
              address: tokenFactoryState.reserveAddress,
              value: fromTokenToUnit(maxReserve, tokenFactoryState.reserveToken.decimal).toString()
            }]
          }] : null
        },
        signatures: [{
          signer: wallet.address,
          pk: wallet.publicKey
        }]
      },
      wallet
    }, extra);
    tx = await client.signMintTokenTx({ tx, wallet });

    return client.sendMintTokenTx({
      tx,
      wallet
    }, extra);
  };

  /**
   * Burn token
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#burnToken
   * @param {object} params
   * @param {string} params.tokenFactory - token factory address
   * @param {number} params.amount - amount of token to burn
   * @param {string} params.receiver - receiver address
   * @param {string} params.minReserve - min reserve received of burning
   * @param {object} params.data - token factory data payload
   * @param {WalletObject} params.wallet - the initial owner of the token factory
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.burnToken = async ({ tokenFactory, amount, receiver, minReserve, data, wallet }, extra) => {
    const { state: tokenFactoryState } = await client.getTokenFactoryState({ address: tokenFactory });

    let tx = await client.multiSignBurnTokenTx({
      tx: {
        itx: {
          tokenFactory,
          minReserve: minReserve ? fromTokenToUnit(minReserve, tokenFactoryState.reserveToken.decimal).toString() : null,
          receiver: receiver || wallet.address,
          data,
          inputs: [{
            owner: wallet.address,
            tokens: [{
              address: tokenFactoryState.tokenAddress,
              value: fromTokenToUnit(amount, tokenFactoryState.token.decimal).toString()
            }]
          }]
        },
        signatures: [{
          signer: wallet.address,
          pk: wallet.publicKey
        }]
      },
      wallet
    }, extra);
    tx = await client.signBurnTokenTx({ tx, wallet });

    return client.sendBurnTokenTx({
      tx,
      wallet
    }, extra);
  };

  /**
   * Burn token
   *
   * @memberof GraphQLClient
   * @function
   * @name GraphQLClient#burnToken
   * @param {object} params
   * @param {string} params.address - token factory address
   * @param {number} params.feeRate - fee for the mint and burn
   * @param {object} params.token - token factory token payload
   * @param {object} params.data - token factory data payload
   * @param {WalletObject} params.wallet - the wallet who is the owner
   * @param {object} extra - other param to underlying client implementation
   * @returns {Promise} the `transactionHash` once resolved
   */
  client.updateTokenFactory = ({ address, feeRate, token, data, wallet }, extra) => {
    const payload = {
      address,
      feeRate,
      token,
      data
    };

    return client.sendUpdateTokenFactoryTx({
      tx: { itx: payload },
      wallet
    }, extra);
  };
};

module.exports = { createExtensionMethods };
//# sourceMappingURL=extension.js.map