Injective Typescript Ethereum Ledger.
Ethereum Ledger
Подписание транзакций на Injective с помощью Ledger
Цель этого документа - объяснить, как использовать Ledger для подписания транзакций на Injective и их трансляции в цепочку. Реализация отличается от стандартного подхода, используемого нативными цепочками Cosmos SDK, поскольку Injective определяет свой собственный тип Account, который использует кривую ECDSA secp256k1 от Ethereum для ключей.
Реализация
Чтобы понять, как нам следует реализовывать этот процесс, давайте рассмотрим некоторые концепции, чтобы было проще понять, какой подход мы будем использовать.
Справочная информация
Путь деривации - это фрагмент данных, который указывает иерархическому детерминированному кошельку (HD), как вывести определенный ключ в дереве ключей. Пути деривации используются в качестве стандарта и были введены в HD-кошельки как часть BIP32. Иерархический детерминированный кошелек - это термин, используемый для описания кошелька, который использует семя для получения множества открытых и закрытых ключей.
Вот как выглядит путь деривации
m/purpose'/coin_type'/account'/change/address_index
Каждая из частей в этой последовательности играет свою роль, и каждая из них изменяет то, каким будет закрытый ключ, открытый ключ и адрес. Мы не будем углубляться в точные детали того, что означает каждая часть пути HD, а просто кратко объясним, что такое coin_type. Каждый блокчейн имеет номер, который его обозначает, т.е. coin_type. Bitcoin - 0, Ethereum - 60, Cosmos - 118.
Специфический контекст Injective
В Injective используется тот же coin_type, что и в Ethereum, т.е. 60. Это означает, что для использования Ledger для подписания транзакций на Injective необходимо использовать приложение Ethereum на Ledger.
На Ledger установлено только одно приложение для одного coin_type. Поскольку для подписания транзакций на Injective нам необходимо использовать приложение Ethereum, мы должны изучить доступные варианты получения действительной подписи. Одним из доступных вариантов является процедура EIP712 для хэширования и подписания типизированных структурированных данных. Ledger раскрывает процедуру signEIP712HashedMessage, которую мы и будем использовать.
После подписания типизированных данных EIP712 мы упакуем транзакцию, используя обычный подход Cosmos-SDK для упаковки и трансляции транзакции. Есть несколько небольших отличий, одно из которых - использование режима SIGN_MODE_LEGACY_AMINO_JSON и добавление Web3Exension к транзакции Cosmos, о которых мы расскажем в этом документе.
EIP712 Типизированные данные
EIP 712 - это стандарт для хэширования и подписи типизированных структурированных данных. Для типизированных данных EIP712 каждое из передаваемых пользователем значений (которые необходимо подписать) имеет представителя типа, который объясняет точный тип этого конкретного значения. В дополнение к значению, которое пользователь хочет подписать, и его типу (PrimaryType типизированных данных EIP712), каждые типизированные данные EIP712 должны содержать EIP712Domain, который предоставляет контекст об источнике транзакции.
Поток транзакций
Сама реализация состоит из нескольких шагов, а именно:
Подготовка транзакции к подписанию с помощью приложения Ethereum на Ledger,
Подготовка и подписание транзакции на Ledger,
Подготовка транзакции к трансляции,
Трансляция транзакции.
Мы подробно рассмотрим каждый шаг и подробно остановимся на действиях, которые необходимо предпринять, чтобы транзакция была подписана и транслирована в цепочку.
Подготовка транзакции (к подписанию)
Как мы уже говорили выше, транзакция должна быть подписана с помощью приложения Ethereum на Ledger. Это означает, что на этапе подписания транзакции пользователю должно быть предложено переключиться (или открыть) приложение Ethereum на Ledger.
Мы знаем, что каждая транзакция Cosmos состоит из сообщений, которые обозначают инструкции, которые пользователь хочет выполнить в цепочке. Если мы хотим отправить средства с одного адреса на другой, мы упаковываем сообщение MsgSend в транзакцию и транслируем его в цепочку.
Зная это, команда Injective сделала абстракцию этих сообщений, чтобы упростить способ их упаковки в транзакцию. Каждое из этих сообщений принимает определенный набор параметров, необходимых для инстанцирования сообщения. После этого абстракция раскрывает несколько удобных методов, которые мы можем использовать в зависимости от выбранного нами метода подписания/трансляции. Например, Message раскрывает метод toDirectSign, который возвращает тип и proto-представление сообщения, которое затем может быть использовано для упаковки транзакции с использованием стандартного подхода Cosmos, подписания ее с помощью privateKey и трансляции в цепочку.
Для нас в данной конкретной реализации важны методы toEip712Types и toEip712. Вызов первого из них на экземпляре Message выдает типы Message для типизированных данных EIP712, а второго - значения Message для данных EIP712. Комбинируя эти два метода, мы можем генерировать корректные типизированные данные EIP712, которые могут быть переданы в процесс подписания.
Итак, давайте посмотрим на фрагмент кода, демонстрирующий использование этих методов и то, как мы можем сгенерировать типизированные данные EIP712 из сообщения:
import { MsgSend, DEFAULT_STD_FEE } from '@injectivelabs/sdk-ts' import { getEip712TypedData, Eip712ConvertTxArgs, Eip712ConvertFeeArgs } from '@injectivelabs/sdk-ts/dist/core/eip712' import { EtherumChainId } from '@injectivelabs/ts-types'
/** More details on these two interfaces later on */ const txArgs: Eip712ConvertTxArgs = { accountNumber: accountDetails.accountNumber.toString(), sequence: accountDetails.sequence.toString(), timeoutHeight: timeoutHeight.toFixed(), chainId: chainId, } const txFeeArgs: Eip712ConvertFeeArgs = DEFAULT_STD_FEE const injectiveAddress = 'inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku' const amount = { amount: new BigNumberInBase(0.01).toWei().toFixed(), denom: "inj", }; const ethereumChainId = EthereumChainId.Mainnet
const msg = MsgSend.fromJSON({ amount, srcInjectiveAddress: injectiveAddress, dstInjectiveAddress: injectiveAddress, });
/** The EIP712 TypedData that can be used for signing **/ const eip712TypedData = getEip712Tx({ msgs: msg, tx: txArgs, fee: txFeeArgs ethereumChainId: ethereumChainId, })
Подготовка к процессу подписания в Ledger
Теперь, когда у нас есть eip712TypedData, необходимо подписать ее с помощью Ledger. Для этого необходимо получить транспорт Ledger в зависимости от поддержки браузера пользователем и с помощью @ledgerhq/hw-app-eth создать экземпляр Ledger с транспортом, который будет использовать приложение Ethereum на устройстве Ledger для выполнения действий пользователя (подтверждения транзакций). После получения eip712TypedData из Шага 1 мы можем использовать signEIP712HashedMessage на EthereumApp для подписания этих typedData и возврата подписи.
import { TypedDataUtils } from 'eth-sig-util' import { bufferToHex, addHexPrefix } from 'ethereumjs-util' import EthereumApp from '@ledgerhq/hw-app-eth'
const domainHash = (message: any) => TypedDataUtils.hashStruct('EIP712Domain', message.domain, message.types, true)
const messageHash = (message: any) => TypedDataUtils.hashStruct( message.primaryType, message.message, message.types, true, )
const transport = /* Get the transport from Ledger */ const ledger = new EthereumApp(transport) const derivationPath = /* Get the derivation path for the address */
/* eip712TypedData from Step 1 */ const object = JSON.parse(eip712TypedData)
const result = await ledger.signEIP712HashedMessage( derivationPath, bufferToHex(domainHash(object)), bufferToHex(messageHash(object)), ) const combined = `${result.r}${result.s}${result.v.toString(16)}` const signature = combined.startsWith('0x') ? combined : `0x${combined}`
Подготовка транзакции к трансляции
Теперь, когда у нас есть подпись, мы можем подготовить транзакцию, используя стандартный подход cosmos.
import { ChainRestAuthApi, ChainRestTendermintApi, BaseAccount, DEFAULT_STD_FEE, createTransaction, createTxRawEIP712, createWeb3Extension, SIGN_AMINO } from '@injectivelabs/sdk-ts' import { DEFAULT_BLOCK_TIMEOUT_HEIGHT } from '@injectivelabs/utils'
const msg: MsgSend /* from Step 1 */
/** Account Details **/ const chainRestAuthApi = new ChainRestAuthApi( lcdEndpoint, ) const accountDetailsResponse = await chainRestAuthApi.fetchAccount( injectiveAddress, ) const baseAccount = BaseAccount.fromRestApi(accountDetailsResponse) const accountDetails = baseAccount.toAccountDetails()
/** Block Details */ const chainRestTendermintApi = new ChainRestTendermintApi( lcdEndpoint, ) const latestBlock = await chainRestTendermintApi.fetchLatestBlock() const latestHeight = latestBlock.header.height const timeoutHeight = new BigNumberInBase(latestHeight).plus( DEFAULT_BLOCK_TIMEOUT_HEIGHT, )
const { txRaw } = createTransaction({ message: msgs, memo: '', signMode: SIGN_AMINO, fee: DEFAULT_STD_FEE, pubKey: publicKeyBase64, sequence: baseAccount.sequence, timeoutHeight: timeoutHeight.toNumber(), accountNumber: baseAccount.accountNumber, chainId: chainId, }) const web3Extension = createWeb3Extension({ ethereumChainId, }) const txRawEip712 = createTxRawEIP712(txRaw, web3Extension)
/** Append Signatures */ const signatureBuff = Buffer.from(signature.replace('0x', ''), 'hex') txRawEip712.signatures = [signatureBuff]
Трансляция транзакции
Теперь, когда транзакция упакована в TxRaw, мы можем транслировать ее на узел, используя стандартный подход cosmos.
Кодовая база
Рассмотрим пример кодовой базы, содержащей все описанные выше шаги
import { ChainRestAuthApi, ChainRestTendermintApi, BaseAccount, DEFAULT_STD_FEE createTransaction, createTxRawEIP712, createWeb3Extension, SIGN_AMINO } from '@injectivelabs/sdk-ts' import { TypedDataUtils } from 'eth-sig-util' import { bufferToHex, addHexPrefix } from 'ethereumjs-util' import EthereumApp from '@ledgerhq/hw-app-eth' import { getEip712TypedData, Eip712ConvertTxArgs, Eip712ConvertFeeArgs } from '@injectivelabs/sdk-ts/dist/core/eip712' import { EtherumChainId, CosmosChainId } from '@injectivelabs/ts-types' import { BigNumberInBase, DEFAULT_BLOCK_TIMEOUT_HEIGHT } from '@injectivelabs/utils'
const domainHash = (message: any) => TypedDataUtils.hashStruct('EIP712Domain', message.domain, message.types, true)
const messageHash = (message: any) => TypedDataUtils.hashStruct( message.primaryType, message.message, message.types, true, )
const signTransaction = async (eip712TypedData: any) => { const transport = /* Get the transport from Ledger */ const ledger = new EthereumApp(transport) const derivationPath = /* Get the derivation path for the address */
/* eip712TypedData from Step 1 */ const result = await ledger.signEIP712HashedMessage( derivationPath, bufferToHex(domainHash(eip712TypedData)), bufferToHex(messageHash(eip712TypedData)), ) const combined = `${result.r}${result.s}${result.v.toString(16)}` const signature = combined.startsWith('0x') ? combined : `0x${combined}`
const getAccountDetails = (address: string): BaseAccount => { const chainRestAuthApi = new ChainRestAuthApi( lcdEndpoint, ) const accountDetailsResponse = await chainRestAuthApi.fetchAccount( address, ) const baseAccount = BaseAccount.fromRestApi(accountDetailsResponse) const accountDetails = baseAccount.toAccountDetails()
const getTimeoutHeight = () => { const chainRestTendermintApi = new ChainRestTendermintApi( lcdEndpoint, ) const latestBlock = await chainRestTendermintApi.fetchLatestBlock() const latestHeight = latestBlock.header.height const timeoutHeight = latestHeight + DEFAULT_BLOCK_TIMEOUT_HEIGHT
const address = 'inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku' const chainId = CosmosChainId.Injective const ethereumChainId = EthereumChainId.Mainnet const accountDetails = getAccountDetails() const timeoutHeight = getTimeoutHeight
const txArgs: Eip712ConvertTxArgs = { accountNumber: accountDetails.accountNumber.toString(), sequence: accountDetails.sequence.toString(), timeoutHeight: timeoutHeight.toString(), chainId: chainId, } const txFeeArgs: Eip712ConvertFeeArgs = DEFAULT_STD_FEE const injectiveAddress = 'inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku' const amount = { amount: new BigNumberInBase(0.01).toWei().toFixed(), denom: "inj", };
const msg = MsgSend.fromJSON({ amount, srcInjectiveAddress: injectiveAddress, dstInjectiveAddress: injectiveAddress, });
/** The EIP712 TypedData that can be used for signing **/ const eip712TypedData = getEip712Tx({ msgs: msg, tx: txArgs, fee: txFeeArgs ethereumChainId: ethereumChainId, })
/** Signing on Ethereum */ const signature = await signTransaction(eip712TypedData)
/** Preparing the transaction for client broadcasting */ const { txRaw } = createTransaction({ message: msg, memo: '', signMode: SIGN_AMINO, fee: DEFAULT_STD_FEE, pubKey: publicKeyBase64, sequence: accountDetails.sequence, timeoutHeight: timeoutHeight.toNumber(), accountNumber: accountDetails.accountNumber, chainId: chainId, }) const web3Extension = createWeb3Extension({ ethereumChainId, }) const txRawEip712 = createTxRawEIP712(txRaw, web3Extension)
/** Append Signatures */ const signatureBuff = Buffer.from(signature.replace('0x', ''), 'hex') txRawEip712.signatures = [signatureBuff]
/** Broadcast the transaction **/ const txRestClient = new TxRestClient(lcdEndpoint) const response = await txRestClient.broadcast(txRawEip712)
if (response.code !== 0) { throw new Error(`Transaction failed: ${response.rawLog}`) }