UNPKG

undedoloremque

Version:
404 lines (359 loc) 11.4 kB
import { Account } from '@/api/account'; import { getPubKeyByPriKey } from '@/keymanage'; import { defaultSignTypedData } from '@/sign/signTx'; import { getGasFeeBySimulate } from '@/utils/units'; import { BaseAccount } from '@bnb-chain/greenfield-cosmos-types/cosmos/auth/v1beta1/auth'; import { Coin } from '@bnb-chain/greenfield-cosmos-types/cosmos/base/v1beta1/coin'; import { ServiceClientImpl, SimulateRequest, } from '@bnb-chain/greenfield-cosmos-types/cosmos/tx/v1beta1/service'; import { AuthInfo, Tx, TxBody, TxRaw, } from '@bnb-chain/greenfield-cosmos-types/cosmos/tx/v1beta1/tx'; import { makeAuthInfoBytes } from '@cosmjs/proto-signing'; import { DeliverTxResponse, StargateClient } from '@cosmjs/stargate'; import { Tendermint37Client } from '@cosmjs/tendermint-rpc'; import { arrayify, hexlify } from '@ethersproject/bytes'; import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util'; import { container, inject, injectable } from 'tsyringe'; import { BroadcastOptions, CustomTx, ISimulateGasFee, MetaTxInfo, SignOptions, SimulateOptions, TxResponse, } from '..'; import { RpcQueryClient } from './queryclient'; import { DEFAULT_DENOM, ZERO_PUBKEY } from '../constants'; import { createEIP712, generateFee, generateMessage, generateTypes, mergeMultiEip712, mergeMultiMessage, } from '../messages'; import { convertAnyTypeData, findAnyType, generateMsg } from '../messages/utils'; import { eip712Hash, makeCosmsPubKey, recoverPk } from '../sign'; export interface ITxClient { /** * simulates a transaction containing the provided messages on the chain. The function returns a pointer to a ISimulateGasFee */ simulateRawTx( txBodyBytes: Uint8Array, accountInfo: BaseAccount, txOption: SimulateOptions, ): Promise<ISimulateGasFee>; /** * broadcasts a transaction containing the provided messages to the chain. The function returns a pointer to a BroadcastTxResponse and any error that occurred during the operation. */ broadcastRawTx(txRawBytes: Uint8Array): Promise<DeliverTxResponse>; tx( typeUrl: MetaTxInfo['typeUrl'], address: MetaTxInfo['address'], MsgSDKTypeEIP712: MetaTxInfo['MsgSDKTypeEIP712'], MsgSDK: MetaTxInfo['MsgSDK'], msgBytes: MetaTxInfo['msgBytes'], ): Promise<TxResponse>; txRaw({ address, txRawHex, eip712MsgType, msgData, }: CustomTx): Promise<Omit<TxResponse, 'metaTxInfo'>>; /** * */ multiTx(txResList: Pick<TxResponse, 'metaTxInfo'>[]): Promise<Omit<TxResponse, 'metaTxInfo'>>; } @injectable() export class TxClient implements ITxClient { public rpcUrl: string; public chainId: string; constructor(@inject('RPC_URL') rpcUrl: string, @inject('CHAIN_ID') chainId: string) { this.rpcUrl = rpcUrl; this.chainId = chainId; } private account: Account = container.resolve(Account); private rpcQueryClient = container.resolve(RpcQueryClient); public async tx( typeUrl: MetaTxInfo['typeUrl'], address: MetaTxInfo['address'], MsgSDKTypeEIP712: MetaTxInfo['MsgSDKTypeEIP712'], MsgSDK: MetaTxInfo['MsgSDK'], msgBytes: MetaTxInfo['msgBytes'], ) { const txBodyBytes = this.getBodyBytes([ { typeUrl, msgBytes, }, ]); const tx = await this.multiTx([ { metaTxInfo: { typeUrl, address, MsgSDKTypeEIP712, MsgSDK, msgBytes, bodyBytes: txBodyBytes, }, }, ]); return { simulate: tx.simulate, broadcast: tx.broadcast, metaTxInfo: { typeUrl, address, MsgSDKTypeEIP712, MsgSDK, msgBytes, bodyBytes: txBodyBytes, }, }; } public async txRaw({ address, txRawHex, eip712MsgType, msgData, }: CustomTx): Promise<Omit<TxResponse, 'metaTxInfo'>> { const accountInfo = await this.account.getAccount(address); const txRawBytes = arrayify(txRawHex); const txRawData = TxRaw.decode(txRawBytes); return { simulate: async (opts: SimulateOptions) => { return await this.simulateRawTx(txRawData.bodyBytes, accountInfo, opts); }, broadcast: async (opts: BroadcastOptions) => { const { denom, gasLimit, gasPrice, payer, granter, privateKey, signTypedDataCallback = defaultSignTypedData, } = opts; const fee = generateFee( String(BigInt(gasLimit) * BigInt(gasPrice)), denom, String(gasLimit), payer, granter, ); // console.log('eip712MsgType', eip712MsgType); const wrapperTypes = generateTypes(eip712MsgType); // find type any and convert const anyFields = findAnyType(msgData); // console.log('anyFields', anyFields); const convertedMsg = convertAnyTypeData(msgData, anyFields); const messages = generateMessage( accountInfo.accountNumber.toString(), accountInfo.sequence.toString(), this.chainId, '', fee, convertedMsg, '0', ); const eip712 = createEIP712(wrapperTypes, this.chainId, messages); // console.log('eip712', eip712); const { pubKey, signature } = privateKey ? this.getSignByPriKey(eip712, privateKey) : await this.getSignByWallet(eip712, accountInfo.address, signTypedDataCallback); const authInfoBytes = this.getAuthInfoBytes({ denom, sequence: accountInfo.sequence + '', gasLimit, gasPrice, pubKey, granter, payer, }); const txRaw = TxRaw.fromPartial({ bodyBytes: txRawData.bodyBytes, authInfoBytes, signatures: [arrayify(signature)], }); const txBytes = TxRaw.encode(txRaw).finish(); // console.log('txBytes', hexlify(txBytes)); return await this.broadcastRawTx(txBytes); }, }; } private getBodyBytes(params: { typeUrl: string; msgBytes: Uint8Array }[]) { const multiMsgBytes = params.map((tx) => { return generateMsg(tx.typeUrl, tx.msgBytes); }); const txBody = TxBody.fromPartial({ messages: multiMsgBytes, }); const txBodyBytes = TxBody.encode(txBody).finish(); return txBodyBytes; } private getSignByPriKey( eip712: ReturnType<typeof createEIP712>, privateKey: SignOptions['privateKey'], ) { const pubKey = getPubKeyByPriKey(privateKey); const signature = signTypedData({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore data: eip712, version: SignTypedDataVersion.V4, privateKey: Buffer.from(arrayify(privateKey)), }); return { pubKey, signature, }; } private async getSignByWallet( eip712: ReturnType<typeof createEIP712>, address: string, signTypedDataCallback: SignOptions['signTypedDataCallback'], ) { const signature = await signTypedDataCallback(address, JSON.stringify(eip712)); const messageHash = eip712Hash(JSON.stringify(eip712)); // console.log('eip712 hash', hexlify(messageHash)); const pk = recoverPk({ signature, messageHash, }); const pubKey = makeCosmsPubKey(pk); return { pubKey, signature, }; } private getAuthInfoBytes( params: Pick<BroadcastOptions, 'denom' | 'gasLimit' | 'gasPrice' | 'granter' | 'payer'> & { pubKey: BaseAccount['pubKey']; sequence: string; }, ) { const { pubKey, denom = DEFAULT_DENOM, sequence, gasLimit, gasPrice, granter, payer } = params; if (!pubKey) throw new Error('pubKey is required'); const feeAmount: Coin[] = [ { denom, amount: String(BigInt(gasLimit) * BigInt(gasPrice)), }, ]; const authInfoBytes = makeAuthInfoBytes( [{ pubkey: pubKey, sequence: Number(sequence) }], feeAmount, gasLimit, granter, payer, 712, ); return authInfoBytes; } public async simulateRawTx( txBodyBytes: Uint8Array, accountInfo: BaseAccount, options: SimulateOptions, ) { const rpcClient = await this.rpcQueryClient.getRpcClient(); const rpc = new ServiceClientImpl(rpcClient); const { denom } = options; const authInfoBytes = this.getAuthInfoBytes({ sequence: accountInfo.sequence + '', denom, gasLimit: 0, gasPrice: '0', pubKey: makeCosmsPubKey(ZERO_PUBKEY), granter: '', payer: '', }); const tx = Tx.fromPartial({ authInfo: AuthInfo.decode(authInfoBytes), body: TxBody.decode(txBodyBytes), signatures: [Uint8Array.from([])], }); const request = SimulateRequest.fromPartial({ txBytes: Tx.encode(tx).finish(), }); const res = await rpc.Simulate(request); return getGasFeeBySimulate(res, denom); } public async broadcastRawTx(txRawBytes: Uint8Array) { const tmClient = await Tendermint37Client.connect(this.rpcUrl); const client = await StargateClient.create(tmClient); return await client.broadcastTx(txRawBytes); } public async multiTx(txResList: Pick<TxResponse, 'metaTxInfo'>[]) { const txs = txResList.map((txRes) => txRes.metaTxInfo); const accountInfo = await this.account.getAccount(txs[0].address); const txBodyBytes = this.getBodyBytes(txs); return { simulate: async (opts: SimulateOptions) => { return await this.simulateRawTx(txBodyBytes, accountInfo, opts); }, broadcast: async (opts: BroadcastOptions) => { const { denom, gasLimit, gasPrice, payer, granter, privateKey, signTypedDataCallback = defaultSignTypedData, } = opts; const types = mergeMultiEip712(txs.map((tx) => tx.MsgSDKTypeEIP712)); const fee = generateFee( String(BigInt(gasLimit) * BigInt(gasPrice)), denom, String(gasLimit), payer, granter, ); const wrapperTypes = generateTypes(types); const multiMessages = mergeMultiMessage(txs); const messages = generateMessage( accountInfo.accountNumber.toString(), accountInfo.sequence.toString(), this.chainId, '', fee, multiMessages, '0', ); const eip712 = createEIP712(wrapperTypes, this.chainId, messages); // console.log('eip712', eip712); const { pubKey, signature } = privateKey ? this.getSignByPriKey(eip712, privateKey) : await this.getSignByWallet(eip712, accountInfo.address, signTypedDataCallback); const authInfoBytes = this.getAuthInfoBytes({ denom, sequence: accountInfo.sequence + '', gasLimit, gasPrice, pubKey, granter, payer, }); const txRaw = TxRaw.fromPartial({ bodyBytes: txBodyBytes, authInfoBytes, signatures: [arrayify(signature)], }); const txBytes = TxRaw.encode(txRaw).finish(); // console.log('txBytes', hexlify(txBytes)); return await this.broadcastRawTx(txBytes); }, }; } }