> ## Documentation Index
> Fetch the complete documentation index at: https://injectivelabs-docs-ai-sdk.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Ethereum Ledger 交易

## 在 Injective 上使用 Ledger 签名交易

本文档的目标是解释如何使用 Ledger 在 Injective 上签名交易并将其广播到链上。该实现与 Cosmos SDK 原生链的默认方法不同，因为 Injective 定义了使用 Ethereum 的 ECDSA secp256k1 曲线作为密钥的自定义账户类型。

## 实现

为了理解我们应该如何实现，让我们先了解一些概念，以便更容易理解我们将采取的方法。

### 背景

派生路径是一段数据，告诉分层确定性（HD）钱包如何在密钥树中派生特定密钥。派生路径作为标准使用，并作为 BIP32 的一部分与 HD 钱包一起引入。分层确定性钱包是一个术语，用于描述使用种子派生许多公钥和私钥的钱包。

派生路径看起来像这样

`m/purpose'/coin_type'/account'/change/address_index`

序列中的每个部分都起作用，每个部分都会改变私钥、公钥和地址。我们不会深入探讨 HD 路径每个部分的确切含义，而只是简要解释 `coin_type`。每个区块链都有一个代表它的数字，即 `coin_type`。Bitcoin 是 `0`，Ethereum 是 `60`，Cosmos 是 `118`。

### Injective 特定上下文

Injective 使用与 Ethereum 相同的 `coin_type`，即 `60`。这意味着要使用 Ledger 在 Injective 上签名交易，**我们必须使用 Ledger 上的 Ethereum 应用程序**。

Ledger 限制为一个 `coin_type` 只能安装一个应用程序。由于我们必须使用 Ethereum 应用程序在 Injective 上签名交易，我们必须探索可用的选项来获取有效签名。可用选项之一是用于哈希和签名类型化结构数据的 `EIP712` 程序。Ledger 暴露了我们将使用的 `signEIP712HashedMessage`。

签名 `EIP712` 类型数据后，我们将使用正常的 Cosmos-SDK 方法打包和广播交易。有一些小的差异，其中之一是使用 `SIGN_MODE_LEGACY_AMINO_JSON` 模式并将 `Web3Exension` 附加到 Cosmos 交易，我们将在本文档中解释它们。

### EIP712 类型数据

EIP 712 是用于哈希和签名类型化结构数据的标准。对于每个 EIP712 类型数据，用户传递的每个值（需要签名的）都有一个类型表示，解释该特定值的确切类型。除了用户想要签名的值及其类型（EIP712 typedData 的 `PrimaryType`）之外，每个 EIP712 类型数据都应包含一个 `EIP712Domain`，提供有关交易来源的上下文。

## 交易流程

实现本身包含几个步骤，即：

1. 准备交易以使用 Ledger 上的 Ethereum 应用程序签名，
2. 在 Ledger 上准备和签名交易，
3. 准备交易以进行广播，
4. 广播交易。

我们将深入每个步骤并详细说明我们需要采取的操作，以使交易签名并广播到链上。

### 准备交易（用于签名）

如上所述，交易需要使用 Ledger 上的 Ethereum 应用程序签名。这意味着一旦用户到达签名阶段，必须提示用户切换（或打开）Ledger 上的 Ethereum 应用程序。

我们知道每个 Cosmos 交易都由消息组成，这些消息表示用户想要在链上执行的指令。如果我们想将资金从一个地址发送到另一个地址，我们将把 `MsgSend` 消息打包到交易中并广播到链上。

知道这一点，Injective 团队对这些消息进行了[抽象](https://github.com/InjectiveLabs/injective-ts/blob/master/packages/sdk-ts/src/core/modules/MsgBase.ts)，以简化它们打包到交易中的方式。每个消息都接受实例化消息所需的特定参数集。完成后，抽象暴露了几个方便的方法，我们可以根据选择的签名/广播方法使用。例如，消息暴露了 `toDirectSign` 方法，该方法返回消息的类型和 proto 表示，然后可以使用默认的 Cosmos 方法打包交易，使用 privateKey 签名并广播到链上。

对于这个特定实现，重要的是 `toEip712Types` 和 `toEip712` 方法。在消息实例上调用第一个方法会给出 EIP712 类型数据的消息类型，第二个方法给出 EIP712 数据的消息值。当我们组合这两个方法时，我们可以生成有效的 EIP712 类型数据，可以传递给签名过程。

所以，让我们看一个使用这些方法的快速代码片段，以及如何从消息生成 EIP712 typedData：

```ts theme={null}
import {
  MsgSend,
} from "@injectivelabs/sdk-ts/core/modules";
import {
  getEip712TypedDataV2,
  type Eip712ConvertTxArgs,
  type Eip712ConvertFeeArgs,
} from "@injectivelabs/sdk-ts/core/tx";
import { EvmChainId } from "@injectivelabs/ts-types";
import { toChainFormat, getDefaultStdFee } from "@injectivelabs/utils";

/** 稍后会详细介绍这两个接口 */
const txArgs: Eip712ConvertTxArgs = {
  accountNumber: accountDetails.accountNumber.toString(),
  sequence: accountDetails.sequence.toString(),
  timeoutHeight: timeoutHeight.toFixed(),
  chainId: chainId,
};
const txFeeArgs: Eip712ConvertFeeArgs = getDefaultStdFee();
const injectiveAddress = "inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku";
const amount = {
  denom: "inj",
  amount: toChainFormat(0.01).toFixed(),
};
const evmChainId = EvmChainId.Mainnet;

const msg = MsgSend.fromJSON({
  amount,
  srcInjectiveAddress: injectiveAddress,
  dstInjectiveAddress: injectiveAddress,
});

/** 可用于签名的 EIP712 TypedData **/
const eip712TypedData = getEip712TypedDataV2({
  msgs: msg,
  tx: txArgs,
  evmChainId,
  fee: txFeeArgs,
});

return eip712TypedData;
```

### 在 Ledger 上准备签名过程

现在我们有了 `eip712TypedData`，我们需要使用 Ledger 签名它。首先，我们需要根据用户在浏览器上的支持获取 Ledger 的 transport，并使用 `@ledgerhq/hw-app-eth` 创建一个 Ledger 实例，该实例将使用 Ledger 设备上的 Ethereum 应用程序执行用户的操作（确认交易）。从步骤 1 获取 `eip712TypedData` 后，我们可以使用 `EthereumApp` 上的 `signEIP712HashedMessage` 签名此 typedData 并返回签名。

```ts theme={null}
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 = /* 从 Ledger 获取 transport */
const ledger = new EthereumApp(transport)
const derivationPath = /* 获取地址的派生路径 */

/* 来自步骤 1 的 eip712TypedData */
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}`

return signature;
```

### 准备交易以进行广播

现在我们有了签名，我们可以使用默认的 cosmos 方法准备交易。

```ts theme={null}
import {
  SIGN_AMINO,
  createTransaction,
  createTxRawEIP712,
  createWeb3Extension,
} from "@injectivelabs/sdk-ts/core/tx";
import {
  BaseAccount,
} from "@injectivelabs/sdk-ts/core/accounts";
import {
  ChainRestAuthApi,
  ChainRestTendermintApi,
} from "@injectivelabs/sdk-ts/client/chain";
import { ChainId, EvmChainId } from "@injectivelabs/ts-types";
import {
  toBigNumber,
  DEFAULT_BLOCK_TIMEOUT_HEIGHT,
} from "@injectivelabs/utils";

const msg: MsgSend; /* 来自步骤 1 */

const chainId = ChainId.Mainnet;
const evmChainId = EvmChainId.Mainnet;

/** 账户详情 **/
const chainRestAuthApi = new ChainRestAuthApi(lcdEndpoint);
const accountDetailsResponse = await chainRestAuthApi.fetchAccount(
  injectiveAddress
);
const baseAccount = BaseAccount.fromRestApi(accountDetailsResponse);
const accountDetails = baseAccount.toAccountDetails();

/** 区块详情 */
const chainRestTendermintApi = new ChainRestTendermintApi(lcdEndpoint);
const latestBlock = await chainRestTendermintApi.fetchLatestBlock();
const latestHeight = latestBlock.header.height;
const timeoutHeight = toBigNumber(latestHeight).plus(
  DEFAULT_BLOCK_TIMEOUT_HEIGHT
);

const { txRaw } = createTransaction({
  message: msgs,
  memo: "",
  signMode: SIGN_AMINO,
  fee: getDefaultStdFee(),
  pubKey: publicKeyBase64,
  sequence: baseAccount.sequence,
  timeoutHeight: timeoutHeight.toNumber(),
  accountNumber: baseAccount.accountNumber,
  chainId,
});
const web3Extension = createWeb3Extension({
  evmChainId,
});
const txRawEip712 = createTxRawEIP712(txRaw, web3Extension);

/** 附加签名 */
const signatureBuff = Buffer.from(signature.replace("0x", ""), "hex");
txRawEip712.signatures = [signatureBuff];

return txRawEip712;
```

### 广播交易

现在我们已将交易打包到 `TxRaw` 中，我们可以使用默认的 cosmos 方法将其广播到节点。

## 代码库

让我们看一个包含上述所有步骤的示例代码库

```ts theme={null}
import {
  TxRestApi,
  SIGN_AMINO,
  createTransaction,
  createTxRawEIP712,
  createWeb3Extension,
  getEip712TypedDataV2,
  type Eip712ConvertTxArgs,
  type Eip712ConvertFeeArgs
} from '@injectivelabs/sdk-ts/core/tx'
import {
  MsgSend,
} from '@injectivelabs/sdk-ts/core/modules'
import {
  BaseAccount,
} from '@injectivelabs/sdk-ts/core/accounts'
import {
  ChainRestAuthApi,
  ChainRestTendermintApi,
} from '@injectivelabs/sdk-ts/client/chain'
import { TypedDataUtils } from 'eth-sig-util'
import EthereumApp from '@ledgerhq/hw-app-eth'
import { bufferToHex, addHexPrefix } from 'ethereumjs-util'
import { EvmChainId, ChainId } from '@injectivelabs/ts-types'
import { toChainFormat, DEFAULT_BLOCK_TIMEOUT_HEIGHT, getDefaultStdFee } 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 = /* 从 Ledger 获取 transport */
  const ledger = new EthereumApp(transport)
  const derivationPath = /* 获取地址的派生路径 */

  /* 来自步骤 1 的 eip712TypedData */
  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}`

  return signature;
}

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()

  return accountDetails
}

const getTimeoutHeight = () => {
  const chainRestTendermintApi = new ChainRestTendermintApi(
    lcdEndpoint,
  )
  const latestBlock = await chainRestTendermintApi.fetchLatestBlock()
  const latestHeight = latestBlock.header.height
  const timeoutHeight = latestHeight + DEFAULT_BLOCK_TIMEOUT_HEIGHT

  return timeoutHeight
}

const address = 'inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku'
const chainId = ChainId.Mainnet
const evmChainId = EvmChainId.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 = getDefaultStdFee()
const injectiveAddress = 'inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku'
const amount = {
  amount: toChainFormat(0.01).toFixed(),
  denom: "inj",
};

const msg = MsgSend.fromJSON({
  amount,
  srcInjectiveAddress: injectiveAddress,
  dstInjectiveAddress: injectiveAddress,
});

/** 可用于签名的 EIP712 TypedData **/
const eip712TypedData = getEip712TypedDataV2({
  msgs: msg,
  tx: txArgs,
  evmChainId,
  fee: txFeeArgs
})

/** 在 Ethereum 上签名 */
const signature = await signTransaction(eip712TypedData)

/** 准备交易以进行客户端广播 */
const { txRaw } = createTransaction({
  message: msg,
  memo: '',
  signMode: SIGN_AMINO,
  fee: getDefaultStdFee(),
  pubKey: publicKeyBase64,
  sequence: accountDetails.sequence,
  timeoutHeight: timeoutHeight.toNumber(),
  accountNumber: accountDetails.accountNumber,
  chainId: chainId,
})
const web3Extension = createWeb3Extension({
  evmChainId,
})
const txRawEip712 = createTxRawEIP712(txRaw, web3Extension)

/** 附加签名 */
const signatureBuff = Buffer.from(signature.replace('0x', ''), 'hex')
txRawEip712.signatures = [signatureBuff]

/** 广播交易 **/
const txRestApi = new TxRestApi(lcdEndpoint)
const response = await txRestApi.broadcast(txRawEip712)

if (response.code !== 0) {
  throw new Error(`Transaction failed: ${response.rawLog}`)
}

return response.txhash

```
