support signning evmos transactions with ledger
This commit is contained in:
parent
899cb0d20d
commit
1a9aafbd60
@ -22,12 +22,14 @@
|
|||||||
"@cosmjs/math": "^0.28.4",
|
"@cosmjs/math": "^0.28.4",
|
||||||
"@cosmjs/proto-signing": "^0.28.4",
|
"@cosmjs/proto-signing": "^0.28.4",
|
||||||
"@cosmjs/stargate": "0.28.4",
|
"@cosmjs/stargate": "0.28.4",
|
||||||
|
"@hanchon/signature-to-pubkey": "^1.0.0",
|
||||||
"@intlify/vue-i18n-loader": "^2.1.2",
|
"@intlify/vue-i18n-loader": "^2.1.2",
|
||||||
"@ledgerhq/hw-app-eth": "^6.28.2",
|
"@ledgerhq/hw-app-eth": "^6.28.2",
|
||||||
"@ledgerhq/hw-transport-web-ble": "^6.27.1",
|
"@ledgerhq/hw-transport-web-ble": "^6.27.1",
|
||||||
"@ledgerhq/hw-transport-webusb": "^6.27.1",
|
"@ledgerhq/hw-transport-webusb": "^6.27.1",
|
||||||
"@tharsis/address-converter": "^0.1.7",
|
"@metamask/eth-sig-util": "^4.0.1",
|
||||||
"@tharsis/transactions": "^0.2.2",
|
"@tharsis/address-converter": "^0.1.8",
|
||||||
|
"@tharsis/transactions": "^0.2.3",
|
||||||
"@vue/composition-api": "^1.4.9",
|
"@vue/composition-api": "^1.4.9",
|
||||||
"@vueuse/core": "4.0.0",
|
"@vueuse/core": "4.0.0",
|
||||||
"animate.css": "4.1.1",
|
"animate.css": "4.1.1",
|
||||||
@ -41,6 +43,8 @@
|
|||||||
"cosmjs-types": "^0.2.0",
|
"cosmjs-types": "^0.2.0",
|
||||||
"dayjs": "^1.10.6",
|
"dayjs": "^1.10.6",
|
||||||
"echarts": "5.3.0",
|
"echarts": "5.3.0",
|
||||||
|
"eth-crypto": "^2.3.0",
|
||||||
|
"ethers": "^5.6.8",
|
||||||
"leaflet": "1.6.0",
|
"leaflet": "1.6.0",
|
||||||
"ledger-cosmos-js": "2.1.8",
|
"ledger-cosmos-js": "2.1.8",
|
||||||
"long": "^5.2.0",
|
"long": "^5.2.0",
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
|
||||||
import { AminoMsg } from "@cosmjs/amino";
|
|
||||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
|
||||||
|
|
||||||
export interface AminoConverter {
|
|
||||||
readonly aminoType: string;
|
|
||||||
readonly toAmino: (value: any) => any;
|
|
||||||
readonly fromAmino: (value: any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A map from protobuf type URL to the AminoConverter implementation if supported on chain */
|
|
||||||
export type AminoConverters = Record<string, AminoConverter | "not_supported_by_chain">;
|
|
||||||
|
|
||||||
function isAminoConverter(
|
|
||||||
converter: [string, AminoConverter | "not_supported_by_chain"],
|
|
||||||
): converter is [string, AminoConverter] {
|
|
||||||
return typeof converter[1] !== "string";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map from Stargate message types as used in the messages's `Any` type
|
|
||||||
* to Amino types.
|
|
||||||
*/
|
|
||||||
export class AminoTypes {
|
|
||||||
// The map type here ensures uniqueness of the protobuf type URL in the key.
|
|
||||||
// There is no uniqueness guarantee of the Amino type identifier in the type
|
|
||||||
// system or constructor. Instead it's the user's responsibility to ensure
|
|
||||||
// there is no overlap when fromAmino is called.
|
|
||||||
private readonly register: Record<string, AminoConverter | "not_supported_by_chain">;
|
|
||||||
|
|
||||||
public constructor(types: AminoConverters) {
|
|
||||||
this.register = types;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toAmino({ typeUrl, value }: EncodeObject): AminoMsg {
|
|
||||||
const converter = this.register[typeUrl];
|
|
||||||
if (converter === "not_supported_by_chain") {
|
|
||||||
throw new Error(
|
|
||||||
`The message type '${typeUrl}' cannot be signed using the Amino JSON sign mode because this is not supported by chain.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!converter) {
|
|
||||||
throw new Error(
|
|
||||||
`Type URL '${typeUrl}' does not exist in the Amino message type register. ` +
|
|
||||||
"If you need support for this message type, you can pass in additional entries to the AminoTypes constructor. " +
|
|
||||||
"If you think this message type should be included by default, please open an issue at https://github.com/cosmos/cosmjs/issues.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: converter.aminoType,
|
|
||||||
value: converter.toAmino(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public fromAmino({ type, value }: AminoMsg): EncodeObject {
|
|
||||||
const matches = Object.entries(this.register)
|
|
||||||
.filter(isAminoConverter)
|
|
||||||
.filter(([_typeUrl, { aminoType }]) => aminoType === type);
|
|
||||||
|
|
||||||
switch (matches.length) {
|
|
||||||
case 0: {
|
|
||||||
throw new Error(
|
|
||||||
`Amino type identifier '${type}' does not exist in the Amino message type register. ` +
|
|
||||||
"If you need support for this message type, you can pass in additional entries to the AminoTypes constructor. " +
|
|
||||||
"If you think this message type should be included by default, please open an issue at https://github.com/cosmos/cosmjs/issues.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case 1: {
|
|
||||||
const [typeUrl, converter] = matches[0];
|
|
||||||
return {
|
|
||||||
typeUrl: typeUrl,
|
|
||||||
value: converter.fromAmino(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw new Error(
|
|
||||||
`Multiple types are registered with Amino type identifier '${type}': '` +
|
|
||||||
matches
|
|
||||||
.map(([key, _value]) => key)
|
|
||||||
.sort()
|
|
||||||
.join("', '") +
|
|
||||||
"'. Thus fromAmino cannot be performed.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,13 +5,18 @@ import Transport from "@ledgerhq/hw-transport";
|
|||||||
|
|
||||||
import Eth from "@ledgerhq/hw-app-eth";
|
import Eth from "@ledgerhq/hw-app-eth";
|
||||||
import { LedgerEthTransactionResolution, LoadConfig } from "@ledgerhq/hw-app-eth/lib/services/types";
|
import { LedgerEthTransactionResolution, LoadConfig } from "@ledgerhq/hw-app-eth/lib/services/types";
|
||||||
import { fromBech32, toHex } from "@cosmjs/encoding";
|
import { fromBech32, fromHex, toBech32, toHex } from "@cosmjs/encoding";
|
||||||
import TransportWebBLE from '@ledgerhq/hw-transport-web-ble'
|
import TransportWebBLE from '@ledgerhq/hw-transport-web-ble'
|
||||||
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
|
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
|
||||||
import { HdPath } from "@cosmjs/crypto";
|
import { HdPath, keccak256, pathToString } from "@cosmjs/crypto";
|
||||||
import { ethToCosmos } from '@tharsis/address-converter'
|
import { ethToEvmos } from '@tharsis/address-converter'
|
||||||
import eth from "@tharsis/proto/dist/proto/ethermint/crypto/v1/ethsecp256k1/keys";
|
import eth from "@tharsis/proto/dist/proto/ethermint/crypto/v1/ethsecp256k1/keys";
|
||||||
|
|
||||||
|
import { Secp256k1 } from "@cosmjs/crypto"
|
||||||
|
import EthCrypto from "eth-crypto"
|
||||||
|
import { MessageTypeProperty, SignTypedDataVersion, TypedDataUtils, TypedMessage, MessageTypes } from "@metamask/eth-sig-util";
|
||||||
|
import { EIPToSign } from "@tharsis/transactions";
|
||||||
|
// import { recoverPublicKey } from "@metamask/eth-sig-util/dist/utils";
|
||||||
// export type Algo = "secp256k1" | "ed25519" | "sr25519" | "ethsecp256k1";
|
// export type Algo = "secp256k1" | "ed25519" | "sr25519" | "ethsecp256k1";
|
||||||
// export interface AccountData {
|
// export interface AccountData {
|
||||||
// /** A printable address (typically bech32 encoded) */
|
// /** A printable address (typically bech32 encoded) */
|
||||||
@ -20,34 +25,35 @@ import eth from "@tharsis/proto/dist/proto/ethermint/crypto/v1/ethsecp256k1/keys
|
|||||||
// readonly pubkey: Uint8Array;
|
// readonly pubkey: Uint8Array;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
export class EthereumLedgerSigner implements OfflineAminoSigner{
|
export class EthereumLedgerSigner implements OfflineAminoSigner{
|
||||||
app: Eth
|
app: Eth
|
||||||
hdpath: string
|
hdpath: string = "44'/60'/0'/0/0"
|
||||||
|
prefix: string = "evmos"
|
||||||
|
|
||||||
static async create(protocol: string, hdpath: string, scrambleKey = "w0w", loadConfig?: LoadConfig): Promise<EthereumLedgerSigner> {
|
static async create(protocol: string, hdpath: HdPath, scrambleKey = "w0w", loadConfig?: LoadConfig): Promise<EthereumLedgerSigner> {
|
||||||
let transport: Promise<Transport> = protocol === 'ledgerBle' ? TransportWebBLE.create() : TransportWebUSB.create()
|
let transport: Promise<Transport> = protocol === 'ledgerBle' ? TransportWebBLE.create() : TransportWebUSB.create()
|
||||||
return transport.then(t => {
|
return transport.then(t => {
|
||||||
let instance = new EthereumLedgerSigner()
|
let instance = new EthereumLedgerSigner()
|
||||||
instance.hdpath = hdpath
|
instance.hdpath = pathToString(hdpath).replace('m/', '').replace('/60/', "/60'/")
|
||||||
instance.app = new Eth(t, scrambleKey, loadConfig)
|
instance.app = new Eth(t, scrambleKey, loadConfig)
|
||||||
return instance
|
return instance
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
async getAccounts(): Promise<readonly AccountData[]> {
|
|
||||||
console.log('eth:', this.app, this.hdpath)
|
public async getAccounts(): Promise<readonly AccountData[]> {
|
||||||
|
return this.app.getAddress(this.hdpath).then(x => {
|
||||||
return this.app.getAddress("44'/60'/0'/0/0").then(x => {
|
|
||||||
const x1: AccountData = {
|
const x1: AccountData = {
|
||||||
pubkey: new TextEncoder().encode(x.publicKey),
|
pubkey: fromHex(EthCrypto.publicKey.compress(x.publicKey)),
|
||||||
address: x.address,
|
address: x.address,
|
||||||
algo: "secp256k1" // should be 'ethsecp256k1'
|
algo: "secp256k1" // should be 'ethsecp256k1'
|
||||||
}
|
}
|
||||||
const x2: AccountData = {
|
const x2: AccountData = {
|
||||||
pubkey: new TextEncoder().encode(x.publicKey),
|
pubkey: fromHex(EthCrypto.publicKey.compress(x.publicKey)),
|
||||||
address: ethToCosmos(x.address),
|
address: toBech32(this.prefix, fromBech32(ethToEvmos(x.address)).data),
|
||||||
algo: "secp256k1" // // should be 'ethsecp256k1'
|
algo: "secp256k1" // should be 'ethsecp256k1'
|
||||||
}
|
}
|
||||||
return [x1, x2]
|
return [x2, x1]
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +70,7 @@ export class EthereumLedgerSigner implements OfflineAminoSigner{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
|
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
|
||||||
const ethAddr = this.goEthAddress(signerAddress)
|
const ethAddr = this.goEthAddress(signerAddress)
|
||||||
return this.getAccounts().then(list => {
|
return this.getAccounts().then(list => {
|
||||||
const acc = list.find(x => x.address === ethAddr)
|
const acc = list.find(x => x.address === ethAddr)
|
||||||
@ -87,15 +93,29 @@ export class EthereumLedgerSigner implements OfflineAminoSigner{
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
async showAddress(path?: HdPath): Promise<AddressAndPubkey> {
|
// async showAddress(path?: HdPath): Promise<AddressAndPubkey> {
|
||||||
return new Promise((r, j) => { })
|
// return new Promise((r, j) => { })
|
||||||
}
|
|
||||||
|
|
||||||
// async sign712Transaction(rawTxHex: string, resolution: LedgerEthTransactionResolution) {
|
|
||||||
// return this.app.signEIP712HashedMessage(this.hdpath, rawTxHex, resolution)
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
async signTransaction(rawTxHex: string, resolution?: LedgerEthTransactionResolution) {
|
// async signTransaction(rawTxHex: string, resolution?: LedgerEthTransactionResolution) {
|
||||||
return this.app.signTransaction(this.hdpath, rawTxHex, resolution)
|
// return this.app.signPersonalMessage(this.hdpath, rawTxHex)
|
||||||
|
// }
|
||||||
|
|
||||||
|
async sign712( eipToSign: EIPToSign ) {
|
||||||
|
|
||||||
|
/// sign typed struct
|
||||||
|
const types = eipToSign.types as Record<string, MessageTypeProperty[]>
|
||||||
|
const domainSeparatorHex = TypedDataUtils.hashStruct('EIP712Domain', eipToSign.domain, types, SignTypedDataVersion.V4).toString('hex')
|
||||||
|
// console.log('hex1: ', domainSeparatorHex)
|
||||||
|
const hashStructMessageHex = toHex(keccak256(TypedDataUtils.encodeData('Tx', eipToSign.message as Record<string, unknown>, types, SignTypedDataVersion.V4)))
|
||||||
|
// console.log('hex2:', hashStructMessageHex)
|
||||||
|
const signature = await this.app.signEIP712HashedMessage(this.hdpath, domainSeparatorHex, hashStructMessageHex)
|
||||||
|
let v: string = (signature.v - 27).toString(16);
|
||||||
|
if (v.length < 2) {
|
||||||
|
v = "0" + v;
|
||||||
|
}
|
||||||
|
const sig = "0x"+signature.r + signature.s + v
|
||||||
|
|
||||||
|
return sig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
134
src/libs/client/MessageAdapter.ts
Normal file
134
src/libs/client/MessageAdapter.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { EncodeObject } from "@cosmjs/proto-signing"
|
||||||
|
import {
|
||||||
|
createMsgWithdrawDelegatorReward,
|
||||||
|
createIBCMsgTransfer,
|
||||||
|
createMsgSend,
|
||||||
|
createMsgBeginRedelegate,
|
||||||
|
createMsgDelegate,
|
||||||
|
createMsgUndelegate,
|
||||||
|
createMsgVote,
|
||||||
|
// createMsgWithdrawValidatorCommission,
|
||||||
|
} from '@tharsis/proto'
|
||||||
|
import {
|
||||||
|
MSG_WITHDRAW_DELEGATOR_REWARD_TYPES,
|
||||||
|
MSG_BEGIN_REDELEGATE_TYPES,
|
||||||
|
MSG_DELEGATE_TYPES,
|
||||||
|
MSG_SEND_TYPES,
|
||||||
|
MSG_UNDELEGATE_TYPES,
|
||||||
|
MSG_VOTE_TYPES,
|
||||||
|
IBC_MSG_TRANSFER_TYPES,
|
||||||
|
MSG_WITHDRAW_VALIDATOR_COMMISSION_TYPES,
|
||||||
|
} from "@tharsis/eip712"
|
||||||
|
|
||||||
|
|
||||||
|
export interface MessageAdapter {
|
||||||
|
toProto(message: EncodeObject): object
|
||||||
|
getTypes: ()=> object
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WithdrawMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgWithdrawDelegatorReward(param.delegatorAddress, param.validatorAddress)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_WITHDRAW_DELEGATOR_REWARD_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WithdrawCommissionMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgWithdrawDelegatorReward(param.delegatorAddress, param.validatorAddress)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_WITHDRAW_VALIDATOR_COMMISSION_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BeginRedelegateMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgBeginRedelegate(
|
||||||
|
param.delegatorAddress,
|
||||||
|
param.validatorSrcAddress,
|
||||||
|
param.validatorDstAddress,
|
||||||
|
param.amount.amount,
|
||||||
|
param.amount.denom)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_BEGIN_REDELEGATE_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DelegateMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgDelegate(param.delegatorAddress, param.validatorAddress, param.amount[0].amount, param.amount[0].denom)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_DELEGATE_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SendMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgSend(param.fromAddress, param.toAddress, param.amount[0].amount, param.amount[0].denom)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_SEND_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UndelegateMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgUndelegate(param.delegatorAddress, param.validatorAddress, param.amount[0].amount, param.amount[0].denom)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_UNDELEGATE_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VoteMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createMsgVote(param.proposalId, param.option, param.voter)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return MSG_VOTE_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IBCMessageAdapter implements MessageAdapter {
|
||||||
|
toProto(message: EncodeObject) {
|
||||||
|
const param = message.value
|
||||||
|
return createIBCMsgTransfer(
|
||||||
|
param.sourcePort,
|
||||||
|
param.sourceChannel,
|
||||||
|
param.amount.amount,
|
||||||
|
param.amount.denom,
|
||||||
|
param.sender,
|
||||||
|
param.receiver,
|
||||||
|
param.revisionNumber,
|
||||||
|
param.revisionHeight,
|
||||||
|
param.timeoutTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getTypes() {
|
||||||
|
return IBC_MSG_TRANSFER_TYPES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const defaultMessageAdapter: Record<string, MessageAdapter> = {
|
||||||
|
"/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward": new WithdrawMessageAdapter(),
|
||||||
|
"/cosmos.staking.v1beta1.MsgDelegate": new DelegateMessageAdapter(),
|
||||||
|
"/cosmos.staking.v1beta1.MsgBeginRedelegate": new BeginRedelegateMessageAdapter(),
|
||||||
|
"/cosmos.staking.v1beta1.MsgUndelegate": new UndelegateMessageAdapter(),
|
||||||
|
"/cosmos.bank.v1beta1.MsgSend": new SendMessageAdapter(),
|
||||||
|
"/cosmos.gov.v1beta1.MsgVote": new VoteMessageAdapter(),
|
||||||
|
"/ibc.applications.transfer.v1.MsgTransfer": new IBCMessageAdapter(),
|
||||||
|
"/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission": new WithdrawCommissionMessageAdapter()
|
||||||
|
}
|
29
src/libs/client/MetaMaskSigner.js
Normal file
29
src/libs/client/MetaMaskSigner.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { pathToString } from '@cosmjs/crypto'
|
||||||
|
import { fromHex } from '@cosmjs/encoding'
|
||||||
|
import { ethToEvmos } from '@tharsis/address-converter'
|
||||||
|
|
||||||
|
export default class MetaMaskSigner {
|
||||||
|
static create(hdpath) {
|
||||||
|
const signer = new MetaMaskSigner()
|
||||||
|
signer.hdpath = pathToString(hdpath).replace('m/', '').replace('/60/', "/60'/")
|
||||||
|
signer.ethereum = window.ethereum
|
||||||
|
return signer
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccounts() {
|
||||||
|
// eth_accounts, eth_requestAccounts
|
||||||
|
return this.ethereum.request({ method: 'eth_accounts' }).then(data => data.map(x => ({
|
||||||
|
pubkey: fromHex(x.substring(2)), // should set to public key
|
||||||
|
address: ethToEvmos(x),
|
||||||
|
algo: 'ethsecp256k1',
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
async sign(signer, eipToSign) {
|
||||||
|
return this.ethereum.request({
|
||||||
|
method: 'eth_signTypedData_v4',
|
||||||
|
params: [signer, JSON.stringify(eipToSign)],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// signAmino: (signerAddress: string, signDoc: StdSignDoc) => Promise<AminoSignResponse>;
|
||||||
|
}
|
@ -2,45 +2,63 @@ import {
|
|||||||
AminoTypes,
|
AminoTypes,
|
||||||
SignerData,
|
SignerData,
|
||||||
SigningStargateClient,
|
SigningStargateClient,
|
||||||
SigningStargateClientOptions,
|
defaultRegistryTypes,
|
||||||
StargateClient
|
|
||||||
} from '@cosmjs/stargate';
|
} from '@cosmjs/stargate';
|
||||||
import { Registry, OfflineSigner, EncodeObject } from '@cosmjs/proto-signing';
|
import { Registry, EncodeObject, TxBodyEncodeObject, makeAuthInfoBytes, GeneratedType } from '@cosmjs/proto-signing';
|
||||||
import { defaultRegistryTypes } from '@cosmjs/stargate';
|
|
||||||
import { LedgerSigner } from '@cosmjs/ledger-amino';
|
import { LedgerSigner } from '@cosmjs/ledger-amino';
|
||||||
import { EthereumLedgerSigner } from './EthereumLedgerSigner';
|
import { EthereumLedgerSigner } from './EthereumLedgerSigner';
|
||||||
import TransportWebUSB from '@ledgerhq/hw-transport-webusb';
|
import TransportWebUSB from '@ledgerhq/hw-transport-webusb';
|
||||||
import TransportWebBLE from '@ledgerhq/hw-transport-web-ble'
|
import TransportWebBLE from '@ledgerhq/hw-transport-web-ble'
|
||||||
import { HdPath, stringToPath } from '@cosmjs/crypto';
|
import { makeSignDoc, OfflineAminoSigner, Pubkey, pubkeyType, StdFee } from "@cosmjs/amino";
|
||||||
import { StdFee } from "@cosmjs/amino";
|
import { TxBody, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
|
||||||
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
|
import {
|
||||||
|
generateMessageWithMultipleTransactions,
|
||||||
// export async function sign(device, chainId, signerAddress, messages, fee, memo, signerData) {
|
MSG_WITHDRAW_DELEGATOR_REWARD_TYPES,
|
||||||
// // let transport
|
IBC_MSG_TRANSFER_TYPES,
|
||||||
// let signer
|
generateTypes,
|
||||||
// const hdpath = getHdPath(signerAddress)
|
generateFee,
|
||||||
// const coinType = Number(hdpath[1])
|
createEIP712,
|
||||||
// switch (device) {
|
} from "@tharsis/eip712"
|
||||||
// case 'ledgerBle':
|
import {
|
||||||
// signer = await getLedgerAppName(coinType, device, hdpath)
|
createTxRawEIP712, signatureToWeb3Extension, Chain,
|
||||||
// break
|
createMessageSend, createTxMsgWithdrawDelegatorReward, Fee, MsgWithdrawDelegatorRewardParams,
|
||||||
// case 'ledgerUSB':
|
createTxMsgMultipleWithdrawDelegatorReward,
|
||||||
// signer = await getLedgerAppName(coinType, device, hdpath)
|
MsgMultipleWithdrawDelegatorRewardParams,
|
||||||
// break
|
EIPToSign,
|
||||||
// case 'keplr':
|
} from '@tharsis/transactions'
|
||||||
// default:
|
import {
|
||||||
// if (!window.getOfflineSigner || !window.keplr) {
|
createTransactionWithMultipleMessages,
|
||||||
// throw new Error('Please install keplr extension')
|
createMsgWithdrawDelegatorReward,
|
||||||
// }
|
createIBCMsgTransfer
|
||||||
// await window.keplr.enable(chainId)
|
} from '@tharsis/proto'
|
||||||
// signer = window.getOfflineSignerOnlyAmino(chainId)
|
import { fromBase64, fromBech32, fromHex, toBase64, toHex } from '@cosmjs/encoding';
|
||||||
// }
|
//import { generateEndpointBroadcast, generatePostBodyBroadcast } from '@tharsis/provider'
|
||||||
|
import { keccak256, Secp256k1 } from "@cosmjs/crypto"
|
||||||
// // Ensure the address has some tokens to spend
|
import { SignMode } from 'cosmjs-types/cosmos/tx/signing/v1beta1/signing';
|
||||||
// const client = await PingWalletClient.offline(signer)
|
import { MessageTypeProperty, MessageTypes, SignTypedDataVersion, TypedDataUtils, TypedMessage, } from '@metamask/eth-sig-util'
|
||||||
// return client.signAmino(device.startsWith('ledger') ? toSignAddress(signerAddress) : signerAddress, messages, fee, memo, signerData)
|
import MetaMaskSigner from './MetaMaskSigner.js'
|
||||||
// }
|
import * as eth from '@tharsis/proto/dist/proto/ethermint/crypto/v1/ethsecp256k1/keys' // /ethermint/crypto/v1/ethsecp256k1/keys'
|
||||||
|
import {
|
||||||
|
AminoConverters,
|
||||||
|
createAuthzAminoConverters,
|
||||||
|
createBankAminoConverters,
|
||||||
|
createDistributionAminoConverters,
|
||||||
|
createFreegrantAminoConverters,
|
||||||
|
createGovAminoConverters,
|
||||||
|
createIbcAminoConverters,
|
||||||
|
createStakingAminoConverters,
|
||||||
|
} from "@cosmjs/stargate"
|
||||||
|
import { Int53 } from '@cosmjs/math';
|
||||||
|
import { Any } from 'cosmjs-types/google/protobuf/any';
|
||||||
|
import { legacyToBuffer, recoverPublicKey } from '@metamask/eth-sig-util/dist/utils';
|
||||||
|
import { utils } from 'ethers'
|
||||||
|
import { signatureToPubkey } from '@hanchon/signature-to-pubkey'
|
||||||
|
import { defaultMessageAdapter } from './MessageAdapter';
|
||||||
|
|
||||||
|
export interface TypedDataField {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
export declare type SigningClient = SigningStargateClient | SigningEthermintClient;
|
export declare type SigningClient = SigningStargateClient | SigningEthermintClient;
|
||||||
|
|
||||||
@ -64,14 +82,176 @@ export async function getSigningClient(device, hdpath): Promise<SigningClient> {
|
|||||||
return SigningStargateClient.offline(signer)
|
return SigningStargateClient.offline(signer)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SigningEthermintClient {
|
function createDefaultTypes(prefix: string): AminoConverters {
|
||||||
readonly signer: OfflineSigner
|
return {
|
||||||
constructor(signer: OfflineSigner) {
|
...createAuthzAminoConverters(),
|
||||||
this.signer = signer
|
...createBankAminoConverters(),
|
||||||
}
|
...createDistributionAminoConverters(),
|
||||||
|
...createGovAminoConverters(),
|
||||||
sign(signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, memo: string, explicitSignerData?: SignerData): Promise<TxRaw> {
|
...createStakingAminoConverters(prefix),
|
||||||
return new Promise(() => { return TxRaw.decode(null) })
|
...createIbcAminoConverters(),
|
||||||
}
|
...createFreegrantAminoConverters(),
|
||||||
|
// ...createVestingAminoConverters(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractChainId(chainId: string) {
|
||||||
|
const start = chainId.indexOf('_')
|
||||||
|
const end = chainId.indexOf('-')
|
||||||
|
if (end > start && start > 0) {
|
||||||
|
return Number(chainId.substring(start + 1, end))
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
export class SigningEthermintClient {
|
||||||
|
readonly signer: EthereumLedgerSigner
|
||||||
|
aminoTypes: AminoTypes
|
||||||
|
constructor(signer: EthereumLedgerSigner) {
|
||||||
|
this.signer = signer
|
||||||
|
this.aminoTypes = new AminoTypes(createDefaultTypes(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
// async signEVM(signerAddress: string, messages: readonly EncodeObject[], stdfee: StdFee, memo: string, explicitSignerData?: SignerData): Promise<Uint8Array> {
|
||||||
|
// const chain: Chain = {
|
||||||
|
// chainId: extractChainId(explicitSignerData.chainId),
|
||||||
|
// cosmosChainId: explicitSignerData.chainId
|
||||||
|
// }
|
||||||
|
// this.signer.prefix = fromBech32(signerAddress).prefix
|
||||||
|
// const account = await this.signer.getAccounts()
|
||||||
|
|
||||||
|
// const sender = {
|
||||||
|
// accountAddress: signerAddress,
|
||||||
|
// sequence: explicitSignerData.sequence,
|
||||||
|
// accountNumber: explicitSignerData.accountNumber,
|
||||||
|
// pubkey: toBase64(account[0].pubkey),
|
||||||
|
// }
|
||||||
|
// const fee = {
|
||||||
|
// amount: stdfee.amount[0].amount,
|
||||||
|
// denom: stdfee.amount[0].denom,
|
||||||
|
// gas: stdfee.gas,
|
||||||
|
// }
|
||||||
|
// const validatorAddresses = messages.map(x => x.value.validatorAddress)
|
||||||
|
|
||||||
|
// let param: MsgMultipleWithdrawDelegatorRewardParams = {
|
||||||
|
// validatorAddresses
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const msgs = createTxMsgMultipleWithdrawDelegatorReward(chain, sender, fee, memo, param)
|
||||||
|
// const sig = await this.signer.sign712(msgs.eipToSign)
|
||||||
|
|
||||||
|
// let extension = signatureToWeb3Extension(chain, sender, sig)
|
||||||
|
|
||||||
|
// // Create the txRaw
|
||||||
|
// let prototx = createTxRawEIP712(msgs.legacyAmino.body, msgs.legacyAmino.authInfo, extension)
|
||||||
|
// return Promise.resolve(prototx.message.serializeBinary())
|
||||||
|
// }
|
||||||
|
|
||||||
|
async sign(signerAddress: string, messages: readonly EncodeObject[], fee: StdFee, memo: string, explicitSignerData?: SignerData): Promise<Uint8Array> {
|
||||||
|
|
||||||
|
const chain: Chain = {
|
||||||
|
chainId: extractChainId(explicitSignerData.chainId),
|
||||||
|
cosmosChainId: explicitSignerData.chainId
|
||||||
|
}
|
||||||
|
|
||||||
|
this.signer.prefix = fromBech32(signerAddress).prefix
|
||||||
|
const account = await this.signer.getAccounts()
|
||||||
|
|
||||||
|
const sender = {
|
||||||
|
accountAddress: signerAddress,
|
||||||
|
sequence: explicitSignerData.sequence,
|
||||||
|
accountNumber: explicitSignerData.accountNumber,
|
||||||
|
pubkey: toBase64(account[0].pubkey),
|
||||||
|
}
|
||||||
|
|
||||||
|
let fees = generateFee(fee.amount[0].amount, fee.amount[0].denom, fee.gas, signerAddress)
|
||||||
|
|
||||||
|
const msgs = messages.map(x => this.aminoTypes.toAmino(x))
|
||||||
|
const tx = generateMessageWithMultipleTransactions(
|
||||||
|
sender.accountNumber.toString(),
|
||||||
|
sender.sequence.toString(),
|
||||||
|
explicitSignerData.chainId,
|
||||||
|
memo,
|
||||||
|
fees,
|
||||||
|
msgs,
|
||||||
|
)
|
||||||
|
|
||||||
|
const types = generateTypes(defaultMessageAdapter[messages[0].typeUrl].getTypes())
|
||||||
|
const eip = createEIP712(types, chain.chainId, tx )
|
||||||
|
const sig = await this.signer.sign712(eip)
|
||||||
|
|
||||||
|
const rawTx = makeRawTxEvmos(sender, messages, memo, fee, sig, chain)
|
||||||
|
|
||||||
|
return Promise.resolve(rawTx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRawTxEvmos(sender, messages, memo, fee, signature, chain): Uint8Array {
|
||||||
|
/// evmos style
|
||||||
|
///*
|
||||||
|
const protoMsgs = messages.map(x => {
|
||||||
|
const adapter = defaultMessageAdapter[x.typeUrl]
|
||||||
|
return adapter.toProto(x)
|
||||||
|
})
|
||||||
|
|
||||||
|
const evmos = createTransactionWithMultipleMessages(
|
||||||
|
protoMsgs,
|
||||||
|
memo,
|
||||||
|
fee.amount[0].amount,
|
||||||
|
fee.amount[0].denom,
|
||||||
|
Number(fee.gas),
|
||||||
|
'ethsecp256',
|
||||||
|
sender.pubkey,
|
||||||
|
sender.sequence,
|
||||||
|
sender.accountNumber,
|
||||||
|
chain.cosmosChainId
|
||||||
|
)
|
||||||
|
|
||||||
|
let extension = signatureToWeb3Extension(chain, sender, signature)
|
||||||
|
|
||||||
|
// Create the txRaw
|
||||||
|
let prototx = createTxRawEIP712(evmos.legacyAmino.body, evmos.legacyAmino.authInfo, extension)
|
||||||
|
return prototx.message.serializeBinary()
|
||||||
|
/// end of EVMOS style */
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRawTx(sender, messages, memo, fee, signature, registry): TxRaw {
|
||||||
|
const pubkey = encodePubkey(sender.pubkey);
|
||||||
|
|
||||||
|
const signedTxBody = {
|
||||||
|
messages,
|
||||||
|
memo,
|
||||||
|
};
|
||||||
|
const signedTxBodyEncodeObject: TxBodyEncodeObject = {
|
||||||
|
typeUrl: "/cosmos.tx.v1beta1.TxBody",
|
||||||
|
value: signedTxBody,
|
||||||
|
};
|
||||||
|
const signedTxBodyBytes = registry.encode(signedTxBodyEncodeObject);
|
||||||
|
const signedGasLimit = Int53.fromString(fee.gas).toNumber();
|
||||||
|
const signedSequence = sender.sequence;
|
||||||
|
const signedAuthInfoBytes = makeAuthInfoBytes(
|
||||||
|
[{ pubkey, sequence: sender.sequence }],
|
||||||
|
fee.amount,
|
||||||
|
signedGasLimit,
|
||||||
|
SignMode.SIGN_MODE_LEGACY_AMINO_JSON,
|
||||||
|
);
|
||||||
|
const rawTx = TxRaw.fromPartial({
|
||||||
|
bodyBytes: signedTxBodyBytes,
|
||||||
|
authInfoBytes: signedAuthInfoBytes,
|
||||||
|
signatures: [fromHex(signature)],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("rawTx", rawTx)
|
||||||
|
return rawTx
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodePubkey(pubkey: string): Any {
|
||||||
|
const value = new eth.ethermint.crypto.v1.ethsecp256k1.PubKey({ key: fromBase64(pubkey) })
|
||||||
|
return Any.fromPartial({
|
||||||
|
typeUrl: "/ethermint.crypto.v1.ethsecp256k1.PubKey",
|
||||||
|
value: value.serializeBinary(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function createAnyMessage(messages: readonly EncodeObject[]) {
|
||||||
|
// return messages.map(x => x.)
|
||||||
|
// }
|
||||||
|
@ -491,7 +491,8 @@ export default class ChainFetch {
|
|||||||
|
|
||||||
// Tx Submit
|
// Tx Submit
|
||||||
async broadcastTx(bodyBytes, config = null) {
|
async broadcastTx(bodyBytes, config = null) {
|
||||||
const txString = toBase64(TxRaw.encode(bodyBytes).finish())
|
const txbytes = bodyBytes.authInfoBytes ? TxRaw.encode(bodyBytes).finish() : bodyBytes
|
||||||
|
const txString = toBase64(txbytes)
|
||||||
const txRaw = {
|
const txRaw = {
|
||||||
tx_bytes: txString,
|
tx_bytes: txString,
|
||||||
mode: 'BROADCAST_MODE_SYNC', // BROADCAST_MODE_SYNC, BROADCAST_MODE_BLOCK, BROADCAST_MODE_ASYNC
|
mode: 'BROADCAST_MODE_SYNC', // BROADCAST_MODE_SYNC, BROADCAST_MODE_BLOCK, BROADCAST_MODE_ASYNC
|
||||||
|
@ -73,6 +73,14 @@
|
|||||||
>
|
>
|
||||||
Ledger via Bluetooth
|
Ledger via Bluetooth
|
||||||
</b-form-radio>
|
</b-form-radio>
|
||||||
|
<b-form-radio
|
||||||
|
v-model="device"
|
||||||
|
name="device"
|
||||||
|
value="metamask"
|
||||||
|
class="mb-1 d-none"
|
||||||
|
>
|
||||||
|
Metamask
|
||||||
|
</b-form-radio>
|
||||||
<b-form-radio
|
<b-form-radio
|
||||||
v-model="device"
|
v-model="device"
|
||||||
name="device"
|
name="device"
|
||||||
@ -321,6 +329,7 @@ import { ValidationProvider, ValidationObserver } from 'vee-validate'
|
|||||||
import ToastificationContent from '@core/components/toastification/ToastificationContent.vue'
|
import ToastificationContent from '@core/components/toastification/ToastificationContent.vue'
|
||||||
// import 'vue-form-wizard/dist/vue-form-wizard.min.css'
|
// import 'vue-form-wizard/dist/vue-form-wizard.min.css'
|
||||||
import 'vue-form-wizard/dist/vue-form-wizard.min.css'
|
import 'vue-form-wizard/dist/vue-form-wizard.min.css'
|
||||||
|
import MetaMaskSigner from '@/libs/client/MetaMaskSigner'
|
||||||
import {
|
import {
|
||||||
BAlert,
|
BAlert,
|
||||||
BRow,
|
BRow,
|
||||||
@ -342,6 +351,7 @@ import {
|
|||||||
addressDecode, addressEnCode, getLedgerAddress, getLocalAccounts,
|
addressDecode, addressEnCode, getLedgerAddress, getLocalAccounts,
|
||||||
} from '@/libs/utils'
|
} from '@/libs/utils'
|
||||||
import { toHex } from '@cosmjs/encoding'
|
import { toHex } from '@cosmjs/encoding'
|
||||||
|
import { stringToPath } from '@cosmjs/crypto'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -524,6 +534,14 @@ export default {
|
|||||||
const offlineSigner = window.getOfflineSigner(chainId)
|
const offlineSigner = window.getOfflineSigner(chainId)
|
||||||
return offlineSigner.getAccounts()
|
return offlineSigner.getAccounts()
|
||||||
},
|
},
|
||||||
|
async connectMetamask() {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
this.debug = 'Please install Metamask extension'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const signer = MetaMaskSigner.create(stringToPath(this.hdpath))
|
||||||
|
return signer.getAccounts()
|
||||||
|
},
|
||||||
localAddress() {
|
localAddress() {
|
||||||
if (!this.address) return false
|
if (!this.address) return false
|
||||||
try {
|
try {
|
||||||
@ -577,6 +595,17 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
case 'metamask':
|
||||||
|
await this.connectMetamask().then(accounts => {
|
||||||
|
if (accounts) {
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
this.accounts = accounts[0]
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
}).catch(e => {
|
||||||
|
this.debug = e
|
||||||
|
})
|
||||||
|
break
|
||||||
case 'ledger':
|
case 'ledger':
|
||||||
case 'ledger2':
|
case 'ledger2':
|
||||||
await this.connect().then(accounts => {
|
await this.connect().then(accounts => {
|
||||||
|
@ -20,6 +20,13 @@
|
|||||||
>
|
>
|
||||||
Keplr
|
Keplr
|
||||||
</b-form-radio>
|
</b-form-radio>
|
||||||
|
<b-form-radio
|
||||||
|
name="wallet"
|
||||||
|
value="metamask"
|
||||||
|
class="d-none d-md-block"
|
||||||
|
>
|
||||||
|
Metamask
|
||||||
|
</b-form-radio>
|
||||||
<b-form-radio
|
<b-form-radio
|
||||||
name="wallet"
|
name="wallet"
|
||||||
value="ledgerUSB"
|
value="ledgerUSB"
|
||||||
|
Loading…
Reference in New Issue
Block a user