diff --git a/examples/sandbox/index.html b/examples/sandbox/index.html index 5f2bc950..6fa302fa 100644 --- a/examples/sandbox/index.html +++ b/examples/sandbox/index.html @@ -193,6 +193,18 @@

Osmosis

+
+

Kujira

+ + + + + + + + +
+

Arkeo

diff --git a/examples/sandbox/index.ts b/examples/sandbox/index.ts index c51e3551..18af80c7 100644 --- a/examples/sandbox/index.ts +++ b/examples/sandbox/index.ts @@ -50,6 +50,14 @@ import { osmosisSwapTx, osmosisUndelegateTx, } from "./json/osmosis/osmosisAminoTx.json"; +import { + kujiraDelegateTx, + kujiraIBCTransferTx, + kujiraRedelegateTx, + kujiraRewardsTx, + kujiraSendTx, + kujiraUndelegateTx, +} from "./json/kujira/kujiraAminoTx.json"; import * as rippleTxJson from "./json/rippleTx.json"; import { thorchainBinanceBaseTx, @@ -1818,6 +1826,180 @@ $osmosisSwap.on("click", async (e) => { } }); +/* + * Kujira + */ +const $kujiraAddress = $("#kujiraAddress"); +const $kujiraSend = $("#kujiraSend"); +const $kujiraDelegate = $("#kujiraDelegate"); +const $kujiraUndelegate = $("#kujiraUndelegate"); +const $kujiraRedelegate = $("#kujiraRedelegate"); +const $kujiraRewards = $("#kujiraRewards"); +const $kujiraIBCTransfer = $("#kujiraIBCTransfer"); + +const $kujiraResults = $("#kujiraResults"); + +$kujiraAddress.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const { addressNList } = wallet.kujiraGetAccountPaths({ accountIdx: 0 })[0]; + const result = await wallet.kujiraGetAddress({ + addressNList, + showDisplay: false, + }); + await wallet.kujiraGetAddress({ + addressNList, + showDisplay: true, + }); + $kujiraResults.val(result); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Lujira"); + } +}); + +$kujiraSend.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const unsigned: core.Kujira.StdTx = kujiraSendTx; + + const res = await wallet.kujiraSignTx({ + addressNList: core.bip32ToAddressNList(`m/44'/118'/0'/0/0`), + chain_id: unsigned.chain_id, + account_number: unsigned.account_number, + sequence: unsigned.sequence, + tx: unsigned, + }); + $kujiraResults.val(JSON.stringify(res)); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Kujira"); + } +}); + +$kujiraDelegate.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const unsigned: core.Kujira.StdTx = kujiraDelegateTx; + + const res = await wallet.kujiraSignTx({ + addressNList: core.bip32ToAddressNList(`m/44'/118'/0'/0/0`), + chain_id: unsigned.chain_id, + account_number: unsigned.account_number, + sequence: unsigned.sequence, + tx: unsigned, + }); + $kujiraResults.val(JSON.stringify(res)); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Kujira"); + } +}); + +$kujiraUndelegate.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const unsigned: core.Kujira.StdTx = kujiraUndelegateTx; + + const res = await wallet.kujiraSignTx({ + addressNList: core.bip32ToAddressNList(`m/44'/118'/0'/0/0`), + chain_id: unsigned.chain_id, + account_number: unsigned.account_number, + sequence: unsigned.sequence, + tx: unsigned, + }); + $kujiraResults.val(JSON.stringify(res)); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Kujira"); + } +}); + +$kujiraRedelegate.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const unsigned: core.Kujira.StdTx = kujiraRedelegateTx; + + const res = await wallet.kujiraSignTx({ + addressNList: core.bip32ToAddressNList(`m/44'/118'/0'/0/0`), + chain_id: unsigned.chain_id, + account_number: unsigned.account_number, + sequence: unsigned.sequence, + tx: unsigned, + }); + $kujiraResults.val(JSON.stringify(res)); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Kujira"); + } +}); + +$kujiraRewards.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const unsigned: core.Kujira.StdTx = kujiraRewardsTx; + + const res = await wallet.kujiraSignTx({ + addressNList: core.bip32ToAddressNList(`m/44'/118'/0'/0/0`), + chain_id: unsigned.chain_id, + account_number: unsigned.account_number, + sequence: unsigned.sequence, + tx: unsigned, + }); + $kujiraResults.val(JSON.stringify(res)); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Kujira"); + } +}); + +$kujiraIBCTransfer.on("click", async (e) => { + e.preventDefault(); + if (!wallet) { + $kujiraResults.val("No wallet?"); + return; + } + if (core.supportsKujira(wallet)) { + const unsigned: core.Kujira.StdTx = kujiraIBCTransferTx; + + const res = await wallet.kujiraSignTx({ + addressNList: core.bip32ToAddressNList(`m/44'/118'/0'/0/0`), + chain_id: unsigned.chain_id, + account_number: unsigned.account_number, + sequence: unsigned.sequence, + tx: unsigned, + }); + $kujiraResults.val(JSON.stringify(res)); + } else { + const label = await wallet.getLabel(); + $kujiraResults.val(label + " does not support Kujira"); + } +}); + /* * Arkeo */ diff --git a/examples/sandbox/json/kujira/kujiraAnomiTx.json b/examples/sandbox/json/kujira/kujiraAnomiTx.json new file mode 100644 index 00000000..593252c8 --- /dev/null +++ b/examples/sandbox/json/kujira/kujiraAnomiTx.json @@ -0,0 +1 @@ +//todo diff --git a/integration/src/kujira/index.ts b/integration/src/kujira/index.ts new file mode 100644 index 00000000..1dc698cd --- /dev/null +++ b/integration/src/kujira/index.ts @@ -0,0 +1,7 @@ +import * as core from "@keepkey/hdwallet-core"; + +import { kujiraTests as tests } from "./kujira"; + +export function kujiraTests(get: () => { wallet: core.HDWallet; info: core.HDWalletInfo }): void { + tests(get); +} diff --git a/packages/hdwallet-core/src/index.ts b/packages/hdwallet-core/src/index.ts index 209fe4a2..1ff4234b 100644 --- a/packages/hdwallet-core/src/index.ts +++ b/packages/hdwallet-core/src/index.ts @@ -11,11 +11,12 @@ export * from "./exceptions"; export * from "./fio"; export * from "./kava"; export * from "./keyring"; +export * from "./kujira"; +export * from "./mayachain"; export * from "./ripple"; export * from "./secret"; export * from "./terra"; export * from "./thorchain"; -export * from "./mayachain"; export * from "./transport"; export * from "./utils"; export * from "./wallet"; diff --git a/packages/hdwallet-core/src/kujira.ts b/packages/hdwallet-core/src/kujira.ts new file mode 100644 index 00000000..5493e863 --- /dev/null +++ b/packages/hdwallet-core/src/kujira.ts @@ -0,0 +1,139 @@ +import { addressNListToBIP32, slip44ByCoin } from "./utils"; +import { BIP32Path, HDWallet, HDWalletInfo, PathDescription } from "./wallet"; + +export interface KujiraGetAddress { + addressNList: BIP32Path; + showDisplay?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Kujira { + export interface Msg { + type: string; + value: any; + } + + export type Coins = Coin[]; + + export interface Coin { + denom: string; + amount: string; + } + + export interface StdFee { + amount: Coins; + gas: string; + } + + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace crypto { + export interface PubKey { + type: string; + value: string; + } + } + + export interface StdSignature { + pub_key?: crypto.PubKey; + signature: string; + } + + export interface StdTx { + msg: Msg[]; + fee: StdFee; + signatures: StdSignature[]; + memo?: string; + } +} + +export interface KujiraTx { + msg: Kujira.Msg[]; + fee: Kujira.StdFee; + signatures: Kujira.StdSignature[]; + memo?: string; +} + +export interface KujiraSignTx { + addressNList: BIP32Path; + tx: Kujira.StdTx; + chain_id: string; + account_number: string; + sequence: string; + fee?: number; +} + +export interface KujiraSignedTx { + serialized: string; + body: string; + authInfoBytes: string; + signatures: string[]; +} + +export interface KujiraGetAccountPaths { + accountIdx: number; +} + +export interface KujiraAccountPath { + addressNList: BIP32Path; +} + +export interface KujiraWalletInfo extends HDWalletInfo { + readonly _supportsKujiraInfo: boolean; + + /** + * Returns a list of bip32 paths for a given account index in preferred order + * from most to least preferred. + */ + KujiraGetAccountPaths(msg: KujiraGetAccountPaths): Array; + + /** + * Returns the "next" account path, if any. + */ + kujiraNextAccountPath(msg: KujiraAccountPath): KujiraAccountPath | undefined; +} + +export interface KujiraWallet extends KujiraWalletInfo, HDWallet { + readonly _supportsKujira: boolean; + + kujiraGetAddress(msg: KujiraGetAddress): Promise; + kujiraSignTx(msg: KujiraSignTx): Promise; +} + +export function kujiraDescribePath(path: BIP32Path): PathDescription { + const pathStr = addressNListToBIP32(path); + const unknown: PathDescription = { + verbose: pathStr, + coin: "Kuji", + isKnown: false, + }; + + if (path.length != 5) { + return unknown; + } + + if (path[0] != 0x80000000 + 44) { + return unknown; + } + + if (path[1] != 0x80000000 + slip44ByCoin("Kuji")) { + return unknown; + } + + if ((path[2] & 0x80000000) >>> 0 !== 0x80000000) { + return unknown; + } + + if (path[3] !== 0 || path[4] !== 0) { + return unknown; + } + + const index = path[2] & 0x7fffffff; + return { + verbose: `Kujira Account #${index}`, + accountIdx: index, + wholeAccount: true, + coin: "Kuji", + isKnown: true, + isPrefork: false, + }; +} diff --git a/packages/hdwallet-core/src/utils.ts b/packages/hdwallet-core/src/utils.ts index 0082c81d..8e22b23b 100644 --- a/packages/hdwallet-core/src/utils.ts +++ b/packages/hdwallet-core/src/utils.ts @@ -160,6 +160,7 @@ const slip44Table = Object.freeze({ ArbitrumNova: 60, Mayachain: 931, Cacao: 931, + Kujira: 118, } as const); type Slip44ByCoin = T extends keyof typeof slip44Table ? typeof slip44Table[T] : number | undefined; export function slip44ByCoin(coin: T): Slip44ByCoin { diff --git a/packages/hdwallet-core/src/wallet.ts b/packages/hdwallet-core/src/wallet.ts index 152f6442..59afc191 100644 --- a/packages/hdwallet-core/src/wallet.ts +++ b/packages/hdwallet-core/src/wallet.ts @@ -9,6 +9,7 @@ import { EosWallet, EosWalletInfo } from "./eos"; import { ETHWallet, ETHWalletInfo } from "./ethereum"; import { FioWallet, FioWalletInfo } from "./fio"; import { KavaWallet, KavaWalletInfo } from "./kava"; +import { KujiraWallet, KujiraWalletInfo } from "./kujira"; import { MayachainWallet, MayachainWalletInfo } from "./mayachain"; import { OsmosisWallet, OsmosisWalletInfo } from "./osmosis"; import { RippleWallet, RippleWalletInfo } from "./ripple"; @@ -173,6 +174,14 @@ export function infoOsmosis(info: HDWalletInfo): info is OsmosisWalletInfo { return isObject(info) && (info as any)._supportsOsmosisInfo; } +export function supportsKujira(wallet: HDWallet): wallet is KujiraWallet { + return isObject(wallet) && (wallet as any)._supportsKujira; +} + +export function infoKujira(info: HDWalletInfo): info is KujiraWalletInfo { + return isObject(info) && (info as any)._supportsKujiraInfo; +} + export function supportsArkeo(wallet: HDWallet): wallet is ArkeoWallet { return isObject(wallet) && (wallet as any)._supportsArkeo; } diff --git a/packages/hdwallet-keepkey/src/keepkey.ts b/packages/hdwallet-keepkey/src/keepkey.ts index 9b781be4..f6678899 100644 --- a/packages/hdwallet-keepkey/src/keepkey.ts +++ b/packages/hdwallet-keepkey/src/keepkey.ts @@ -9,6 +9,7 @@ import * as Btc from "./bitcoin"; import * as Cosmos from "./cosmos"; import * as Eos from "./eos"; import * as Eth from "./ethereum"; +import * as Kujira from "./kujira"; import * as Mayachain from "./mayachain"; import * as Osmosis from "./osmosis"; import * as Ripple from "./ripple"; @@ -246,6 +247,45 @@ function describeOsmosisPath(path: core.BIP32Path): core.PathDescription { }; } +function describeKujiraPath(path: core.BIP32Path): core.PathDescription { + const pathStr = core.addressNListToBIP32(path); + const unknown: core.PathDescription = { + verbose: pathStr, + coin: "Kuji", + isKnown: false, + }; + + if (path.length != 5) { + return unknown; + } + + if (path[0] != 0x80000000 + 44) { + return unknown; + } + + if (path[1] != 0x80000000 + core.slip44ByCoin("Kuji")) { + return unknown; + } + + if ((path[2] & 0x80000000) >>> 0 !== 0x80000000) { + return unknown; + } + + if (path[3] !== 0 || path[4] !== 0) { + return unknown; + } + + const index = path[2] & 0x7fffffff; + return { + verbose: `Kujira Account #${index}`, + accountIdx: index, + wholeAccount: true, + coin: "Kuji", + isKnown: true, + isPrefork: false, + }; +} + function describeThorchainPath(path: core.BIP32Path): core.PathDescription { const pathStr = core.addressNListToBIP32(path); const unknown: core.PathDescription = { @@ -408,6 +448,7 @@ export class KeepKeyHDWalletInfo core.BTCWalletInfo, core.ETHWalletInfo, core.CosmosWalletInfo, + core.KujiraWalletInfo, core.BinanceWalletInfo, core.RippleWalletInfo, core.EosWalletInfo, @@ -418,6 +459,7 @@ export class KeepKeyHDWalletInfo readonly _supportsETHInfo = true; readonly _supportsCosmosInfo = true; readonly _supportsOsmosisInfo = true; + readonly _supportsKujiraInfo = true; readonly _supportsRippleInfo = true; readonly _supportsBinanceInfo = true; readonly _supportsEosInfo = true; @@ -480,6 +522,10 @@ export class KeepKeyHDWalletInfo return Osmosis.osmosisGetAccountPaths(msg); } + public kujiraGetAccountPaths(msg: core.KujieaGetAccountPaths): Array { + return Kujira.kujiraGetAccountPaths(msg); + } + public thorchainGetAccountPaths(msg: core.ThorchainGetAccountPaths): Array { return Thorchain.thorchainGetAccountPaths(msg); } @@ -541,6 +587,8 @@ export class KeepKeyHDWalletInfo return describeCosmosPath(msg.path); case "Osmo": return describeOsmosisPath(msg.path); + case "Kuji": + return describeKujiraPath(msg.path); case "Binance": return describeBinancePath(msg.path); case "Ripple": @@ -625,6 +673,21 @@ export class KeepKeyHDWalletInfo }; } + public kujiraNextAccountPath(msg: core.KujiraAccountPath): core.KujiraAccountPath | undefined { + const description = describeKujiraPath(msg.addressNList); + if (!description.isKnown) { + return undefined; + } + + const addressNList = msg.addressNList; + addressNList[2] += 1; + + return { + ...msg, + addressNList, + }; + } + public thorchainNextAccountPath(msg: core.ThorchainAccountPath): core.ThorchainAccountPath | undefined { const description = describeThorchainPath(msg.addressNList); if (!description.isKnown) { @@ -705,6 +768,7 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW readonly _supportsBTCInfo = true; readonly _supportsCosmosInfo = true; readonly _supportsOsmosisInfo = true; + readonly _supportsKujiraInfo = true; readonly _supportsRippleInfo = true; readonly _supportsBinanceInfo = true; readonly _supportsEosInfo = true; @@ -1325,6 +1389,18 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW return Osmosis.osmosisSignTx(this.transport, msg); } + public kujiraGetAccountPaths(msg: core.KujiraGetAccountPaths): Array { + return this.info.kujiraGetAccountPaths(msg); + } + + public kujiraGetAddress(msg: core.KujiraGetAddress): Promise { + return Kujira.kujiraGetAddress(this.transport, msg); + } + + public kujiraSignTx(msg: core.KujiraSignTx): Promise { + return Kujira.kujiraSignTx(this.transport, msg); + } + public thorchainGetAccountPaths(msg: core.ThorchainGetAccountPaths): Array { return this.info.thorchainGetAccountPaths(msg); } @@ -1401,6 +1477,10 @@ export class KeepKeyHDWallet implements core.HDWallet, core.BTCWallet, core.ETHW return this.info.osmosisNextAccountPath(msg); } + public kujiraNextAccountPath(msg: core.KujiraAccountPath): core.KujiraAccountPath | undefined { + return this.info.kujiraNextAccountPath(msg); + } + public rippleNextAccountPath(msg: core.RippleAccountPath): core.RippleAccountPath | undefined { return this.info.rippleNextAccountPath(msg); } diff --git a/packages/hdwallet-keepkey/src/kujira.ts b/packages/hdwallet-keepkey/src/kujira.ts new file mode 100644 index 00000000..54af89e3 --- /dev/null +++ b/packages/hdwallet-keepkey/src/kujira.ts @@ -0,0 +1,215 @@ +import type { AminoSignResponse, OfflineAminoSigner, StdSignDoc, StdTx } from "@cosmjs/amino"; +import type { AccountData } from "@cosmjs/proto-signing"; +import type { SignerData } from "@cosmjs/stargate"; +import * as Messages from "@keepkey/device-protocol/lib/messages_pb"; +import * as KujiraMessages from "@keepkey/device-protocol/lib/messages-kujira_pb"; +import * as core from "@keepkey/hdwallet-core"; +import bs58check from "bs58check"; +import PLazy from "p-lazy"; + +import { Transport } from "./transport"; + +const protoTxBuilder = PLazy.from(() => import("@keepkey/proto-tx-builder")); + +export function kujiraGetAccountPaths(msg: core.KujiraGetAccountPaths): Array { + return [ + { + addressNList: [0x80000000 + 44, 0x80000000 + core.slip44ByCoin("Kuji"), 0x80000000 + msg.accountIdx, 0, 0], + }, + ]; +} + +export async function kujiraGetAddress( + transport: Transport, + msg: KujiraMessages.KujiraGetAddress.AsObject +): Promise { + const getAddr = new KujiraMessages.KujiraGetAddress(); + getAddr.setAddressNList(msg.addressNList); + getAddr.setShowDisplay(msg.showDisplay !== false); + const response = await transport.call(Messages.MessageType.MESSAGETYPE_KUJIRAGETADDRESS, getAddr, { + msgTimeout: core.LONG_TIMEOUT, + }); + + const kujiraAddress = response.proto as KujiraMessages.KujiraAddress; + return core.mustBeDefined(kujiraAddress.getAddress()); +} + +export async function kujiraSignTx(transport: Transport, msg: core.KujiraSignTx): Promise { + const address = await kujiraGetAddress(transport, { addressNList: msg.addressNList }); + + const getPublicKeyMsg = new Messages.GetPublicKey(); + getPublicKeyMsg.setAddressNList(msg.addressNList); + getPublicKeyMsg.setEcdsaCurveName("secp256k1"); + + const pubkeyMsg = ( + await transport.call(Messages.MessageType.MESSAGETYPE_GETPUBLICKEY, getPublicKeyMsg, { + msgTimeout: core.DEFAULT_TIMEOUT, + }) + ).proto as Messages.PublicKey; + const pubkey = bs58check.decode(core.mustBeDefined(pubkeyMsg.getXpub())).slice(45); + + return transport.lockDuring(async () => { + const signTx = new KujiraMessages.KujiraSignTx(); + signTx.setAddressNList(msg.addressNList); + signTx.setAccountNumber(msg.account_number); + signTx.setChainId(msg.chain_id); + signTx.setFeeAmount(parseInt(msg.tx.fee.amount[0].amount)); + signTx.setGas(parseInt(msg.tx.fee.gas)); + signTx.setSequence(msg.sequence); + if (msg.tx.memo !== undefined) { + signTx.setMemo(msg.tx.memo); + } + signTx.setMsgCount(1); + + let resp = await transport.call(Messages.MessageType.MESSAGETYPE_KUJIRASIGNTX, signTx, { + msgTimeout: core.LONG_TIMEOUT, + omitLock: true, + }); + + for (const m of msg.tx.msg) { + if (resp.message_enum !== Messages.MessageType.MESSAGETYPE_KUJIRAMSGREQUEST) { + throw new Error(`kujira: unexpected response ${resp.message_type}`); + } + + let ack; + + if (m.type === "cosmos-sdk/MsgSend") { + if (m.value.amount.length !== 1) { + throw new Error("kujira: Multiple amounts per msg not supported"); + } + + const denom = m.value.amount[0].denom; + if (denom !== "ukuji") { + throw new Error("kujira: Unsupported denomination: " + denom); + } + + const send = new KujiraMessages.KujiraMsgSend(); + send.setFromAddress(m.value.from_address); + send.setToAddress(m.value.to_address); + send.setAmount(m.value.amount[0].amount); + + ack = new KujiraMessages.KujiraMsgAck(); + ack.setSend(send); + } else if (m.type === "cosmos-sdk/MsgDelegate") { + const denom = m.value.amount.denom; + if (denom !== "ukuji") { + throw new Error("kujira: Unsupported denomination: " + denom); + } + + const delegate = new KujiraMessages.KujiraMsgDelegate(); + delegate.setDelegatorAddress(m.value.delegator_address); + delegate.setValidatorAddress(m.value.validator_address); + delegate.setAmount(m.value.amount.amount); + + ack = new KujiraMessages.KujiraMsgAck(); + + ack.setDelegate(delegate); + } else if (m.type === "cosmos-sdk/MsgUndelegate") { + const denom = m.value.amount.denom; + if (denom !== "ukuji") { + throw new Error("kujira: Unsupported denomination: " + denom); + } + + const undelegate = new KujiraMessages.KujiraMsgUndelegate(); + undelegate.setDelegatorAddress(m.value.delegator_address); + undelegate.setValidatorAddress(m.value.validator_address); + undelegate.setAmount(m.value.amount.amount); + + ack = new KujiraMessages.KujiraMsgAck(); + ack.setUndelegate(undelegate); + } else if (m.type === "cosmos-sdk/MsgBeginRedelegate") { + const denom = m.value.amount.denom; + if (denom !== "ukuji") { + throw new Error("kujira: Unsupported denomination: " + denom); + } + + const redelegate = new KujiraMessages.KujiraMsgRedelegate(); + redelegate.setDelegatorAddress(m.value.delegator_address); + redelegate.setValidatorSrcAddress(m.value.validator_src_address); + redelegate.setValidatorDstAddress(m.value.validator_dst_address); + redelegate.setAmount(m.value.amount.amount); + + ack = new KujiraMessages.KujiraMsgAck(); + ack.setRedelegate(redelegate); + } else if (m.type === "cosmos-sdk/MsgWithdrawDelegationReward") { + const rewards = new KujiraMessages.KujiraMsgRewards(); + rewards.setDelegatorAddress(m.value.delegator_address); + rewards.setValidatorAddress(m.value.validator_address); + if (m.value.amount) { + const denom = m.value.amount.denom; + if (denom !== "ukuji") { + throw new Error("kujira: Unsupported denomination: " + denom); + } + rewards.setAmount(m.value.amount.amount); + } + + ack = new KujiraMessages.KujiraMsgAck(); + ack.setRewards(rewards); + } else if (m.type === "cosmos-sdk/MsgTransfer") { + const denom = m.value.token.denom; + if (denom !== "ukuji") { + throw new Error("kujira: Unsupported denomination: " + denom); + } + + const ibcTransfer = new KujiraMessages.KujiraMsgIBCTransfer(); + ibcTransfer.setReceiver(m.value.receiver); + ibcTransfer.setSender(m.value.sender); + ibcTransfer.setSourceChannel(m.value.source_channel); + ibcTransfer.setSourcePort(m.value.source_port); + ibcTransfer.setRevisionHeight(m.value.timeout_height.revision_height); + ibcTransfer.setRevisionNumber(m.value.timeout_height.revision_number); + ibcTransfer.setAmount(m.value.token.amount); + ibcTransfer.setDenom(m.value.token.denom); + + ack = new KujiraMessages.KujiraMsgAck(); + ack.setIbcTransfer(ibcTransfer); + } else { + throw new Error(`kujira: Message ${m.type} is not yet supported`); + } + + resp = await transport.call(Messages.MessageType.MESSAGETYPE_KUJIRAMSGACK, ack, { + msgTimeout: core.LONG_TIMEOUT, + omitLock: true, + }); + } + + if (resp.message_enum !== Messages.MessageType.MESSAGETYPE_KUJIRASIGNEDTX) { + throw new Error(`kujira: unexpected response ${resp.message_type}`); + } + + const signedTx = resp.proto as KujiraMessages.KujiraSignedTx; + + const offlineSigner: OfflineAminoSigner = { + async getAccounts(): Promise { + return [ + { + address, + algo: "secp256k1", + pubkey, + }, + ]; + }, + async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise { + if (signerAddress !== address) throw new Error("expected signerAddress to match address"); + return { + signed: signDoc, + signature: { + pub_key: { + type: "tendermint/PubKeySecp256k1", + value: signedTx.getPublicKey_asB64(), + }, + signature: signedTx.getSignature_asB64(), + }, + }; + }, + }; + + const signerData: SignerData = { + sequence: Number(msg.sequence), + accountNumber: Number(msg.account_number), + chainId: msg.chain_id, + }; + + return (await protoTxBuilder).sign(address, msg.tx as StdTx, offlineSigner, signerData); + }); +} diff --git a/packages/hdwallet-keepkey/src/typeRegistry.ts b/packages/hdwallet-keepkey/src/typeRegistry.ts index d12bade3..cb3c3bca 100644 --- a/packages/hdwallet-keepkey/src/typeRegistry.ts +++ b/packages/hdwallet-keepkey/src/typeRegistry.ts @@ -2,6 +2,7 @@ import * as Messages from "@keepkey/device-protocol/lib/messages_pb"; import * as BinanceMessages from "@keepkey/device-protocol/lib/messages-binance_pb"; import * as CosmosMessages from "@keepkey/device-protocol/lib/messages-cosmos_pb"; import * as EosMessages from "@keepkey/device-protocol/lib/messages-eos_pb"; +import * as KujiraMessages from "@keepkey/device-protocol/lib/messages-kujira_pb"; import * as MayachainMessages from "@keepkey/device-protocol/lib/messages-mayachain_pb"; import * as NanoMessages from "@keepkey/device-protocol/lib/messages-nano_pb"; import * as RippleMessages from "@keepkey/device-protocol/lib/messages-ripple_pb"; @@ -20,7 +21,8 @@ const AllMessages = ([] as Array<[string, core.Constructor]>) .concat(Object.entries(NanoMessages)) .concat(Object.entries(_.omit(EosMessages, "EosPublicKeyKind", "EosPublicKeyKindMap"))) .concat(Object.entries(ThorchainMessages)) - .concat(Object.entries(MayachainMessages)); + .concat(Object.entries(MayachainMessages)) + .concat(Object.entries(KujiraMessages)); const upperCasedMessageClasses = AllMessages.reduce((registry, entry: [string, core.Constructor]) => { registry[entry[0].toUpperCase()] = entry[1]; diff --git a/packages/hdwallet-native/src/kujira.test.ts b/packages/hdwallet-native/src/kujira.test.ts new file mode 100644 index 00000000..593252c8 --- /dev/null +++ b/packages/hdwallet-native/src/kujira.test.ts @@ -0,0 +1 @@ +//todo diff --git a/packages/hdwallet-native/src/kujira.ts b/packages/hdwallet-native/src/kujira.ts new file mode 100644 index 00000000..2dc0f6a0 --- /dev/null +++ b/packages/hdwallet-native/src/kujira.ts @@ -0,0 +1,99 @@ +import { StdTx } from "@cosmjs/amino"; +import { SignerData } from "@cosmjs/stargate"; +import * as core from "@keepkey/hdwallet-core"; +import * as bech32 from "bech32"; +import CryptoJS from "crypto-js"; +import PLazy from "p-lazy"; + +import * as Isolation from "./crypto/isolation"; +import { NativeHDWalletBase } from "./native"; +import * as util from "./util"; + +const KUJIRA_CHAIN = "kaiyo-1"; + +const protoTxBuilder = PLazy.from(() => import("@keepkey/proto-tx-builder")); + +export function MixinNativeKujiraWalletInfo>(Base: TBase) { + // eslint-disable-next-line @typescript-eslint/no-shadow + return class MixinNativeKujiraWalletInfo extends Base implements core.KujiraWalletInfo { + readonly _supportsKujiraInfo = true; + async kujiraSupportsNetwork(): Promise { + return true; + } + + async kujiraSupportsSecureTransfer(): Promise { + return false; + } + + kujiraSupportsNativeShapeShift(): boolean { + return false; + } + + kujiraGetAccountPaths(msg: core.KujiraGetAccountPaths): Array { + const slip44 = core.slip44ByCoin("Kuji"); + return [ + { + addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx, 0, 0], + }, + ]; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + kujiraNextAccountPath(msg: core.KujiraAccountPath): core.KujiraAccountPath | undefined { + // Only support one account for now. + return undefined; + } + }; +} + +export function MixinNativeKujiraWallet>(Base: TBase) { + // eslint-disable-next-line @typescript-eslint/no-shadow + return class MixinNativeKujiraWallet extends Base { + readonly _supportsKujira = true; + + #masterKey: Isolation.Core.BIP32.Node | undefined; + + async kujiraInitializeWallet(masterKey: Isolation.Core.BIP32.Node): Promise { + this.#masterKey = masterKey; + } + + kujiraWipe(): void { + this.#masterKey = undefined; + } + + kujiraBech32ify(address: ArrayLike, prefix: string): string { + const words = bech32.toWords(address); + return bech32.encode(prefix, words); + } + + createKujiraAddress(publicKey: string) { + const message = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(publicKey)); + const hash = CryptoJS.RIPEMD160(message as any).toString(); + const address = Buffer.from(hash, `hex`); + return this.kujiraBech32ify(address, `kujira`); + } + + async kujiraGetAddress(msg: core.KujiraGetAddress): Promise { + return this.needsMnemonic(!!this.#masterKey, async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const keyPair = await util.getKeyPair(this.#masterKey!, msg.addressNList, "kujira"); + return this.createKujiraAddress(keyPair.publicKey.toString("hex")); + }); + } + + async kujiraSignTx(msg: core.KujiraSignTx): Promise { + return this.needsMnemonic(!!this.#masterKey, async () => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const keyPair = await util.getKeyPair(this.#masterKey!, msg.addressNList, "osmosis"); + const adapter = await Isolation.Adapters.CosmosDirect.create(keyPair.node, "osmo"); + + const signerData: SignerData = { + sequence: Number(msg.sequence), + accountNumber: Number(msg.account_number), + chainId: KUJIRA_CHAIN, + }; + return (await protoTxBuilder).sign(adapter.address, msg.tx as StdTx, adapter, signerData); + }); + } + }; +} diff --git a/packages/hdwallet-native/src/native.test.ts b/packages/hdwallet-native/src/native.test.ts index 248e9708..eed7968b 100644 --- a/packages/hdwallet-native/src/native.test.ts +++ b/packages/hdwallet-native/src/native.test.ts @@ -100,6 +100,10 @@ describe("NativeHDWalletInfo", () => { msg: { coin: "Osmo", path: [44 + 0x80000000, 118 + 0x80000000, 0 + 0x80000000, 0, 0] }, out: { coin: "Osmo", verbose: "Osmosis Account #0", isKnown: true }, }, + { + msg: { coin: "Kuji", path: [44 + 0x80000000, 118 + 0x80000000, 0 + 0x80000000, 0, 0] }, + out: { coin: "Kuji", verbose: "Kujira Account #0", isKnown: true }, + }, { msg: { coin: "cacao", path: [44 + 0x80000000, 931 + 0x80000000, 0 + 0x80000000, 0, 0] }, out: { coin: "Mayachain", verbose: "Mayachain Account #0", isKnown: true }, diff --git a/packages/hdwallet-native/src/native.ts b/packages/hdwallet-native/src/native.ts index e6965682..c520a586 100644 --- a/packages/hdwallet-native/src/native.ts +++ b/packages/hdwallet-native/src/native.ts @@ -12,6 +12,7 @@ import * as Isolation from "./crypto/isolation"; import { MixinNativeETHWallet, MixinNativeETHWalletInfo } from "./ethereum"; import { MixinNativeFioWallet, MixinNativeFioWalletInfo } from "./fio"; import { MixinNativeKavaWallet, MixinNativeKavaWalletInfo } from "./kava"; +import { MixinNativeKujiraWallet, MixinNativeKujiraWalletInfo } from "./kujira"; import { MixinNativeMayachainWallet, MixinNativeMayachainWalletInfo } from "./mayachain"; import { getNetwork } from "./networks"; import { MixinNativeOsmosisWallet, MixinNativeOsmosisWalletInfo } from "./osmosis"; @@ -129,7 +130,7 @@ class NativeHDWalletInfo MixinNativeTerraWalletInfo( MixinNativeKavaWalletInfo( MixinNativeArkeoWalletInfo( - MixinNativeOsmosisWalletInfo(MixinNativeMayachainWalletInfo(NativeHDWalletBase)) + MixinNativeOsmosisWalletInfo(MixinNativeMayachainWalletInfo(MixinNativeKujiraWalletInfo(NativeHDWalletBase))) ) ) ) @@ -183,6 +184,9 @@ class NativeHDWalletInfo case "osmosis": case "osmo": return core.osmosisDescribePath(msg.path); + case "kujira": + case "kuji": + return core.kujiraDescribePath(msg.path); case "fio": return core.fioDescribePath(msg.path); case "arkeo": @@ -208,7 +212,7 @@ export class NativeHDWallet MixinNativeSecretWallet( MixinNativeTerraWallet( MixinNativeKavaWallet( - MixinNativeOsmosisWallet(MixinNativeArkeoWallet(MixinNativeMayachainWallet(NativeHDWalletInfo))) + MixinNativeOsmosisWallet(MixinNativeArkeoWallet(MixinNativeMayachainWallet(MixinNativeKujiraWallet(NativeHDWalletInfo)))) ) ) ) @@ -224,6 +228,7 @@ export class NativeHDWallet core.ETHWallet, core.CosmosWallet, core.OsmosisWallet, + core.KujiraWallet, core.FioWallet, core.ThorchainWallet, core.SecretWallet, @@ -252,6 +257,7 @@ export class NativeHDWallet readonly _supportsKava = true; readonly _supportsArkeo = true; readonly _supportsMayachain = true; + readonly _supportsKujira = true; readonly _isNative = true; #deviceId: string; @@ -338,6 +344,7 @@ export class NativeHDWallet super.ethInitializeWallet(masterKey), super.cosmosInitializeWallet(masterKey), super.osmosisInitializeWallet(masterKey), + super.kujiraInitializeWallet(masterKey), super.binanceInitializeWallet(masterKey), super.fioInitializeWallet(masterKey), super.thorchainInitializeWallet(masterKey), @@ -387,6 +394,7 @@ export class NativeHDWallet super.ethWipe(); super.cosmosWipe(); super.osmosisWipe(); + super.kujiraWipe(); super.binanceWipe(); super.fioWipe(); super.thorchainWipe(); diff --git a/packages/hdwallet-native/src/networks.ts b/packages/hdwallet-native/src/networks.ts index 8947a9d5..c4887cfe 100644 --- a/packages/hdwallet-native/src/networks.ts +++ b/packages/hdwallet-native/src/networks.ts @@ -171,6 +171,7 @@ for (const coin of [ "cardano", "cosmos", "osmosis", + "kujira", "binance", "ethereum", "arkeo",