Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: LIVE-12068 [cosmos]Adapt LL code to avoid using deprecated endpoints #6735

Merged
merged 10 commits into from
May 15, 2024
5 changes: 5 additions & 0 deletions .changeset/neat-ads-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-common": patch
---

fix cosmos code to avoid using deprecated endpoints
41 changes: 30 additions & 11 deletions libs/ledger-live-common/src/families/cosmos/api/Cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { patchOperationWithHash } from "../../../operation";
import cryptoFactory from "../chain/chain";
import cosmosBase from "../chain/cosmosBase";
import * as CosmosSDKTypes from "./types";
import semver from "semver";
import {
CosmosDelegation,
CosmosDelegationStatus,
Expand All @@ -23,6 +24,13 @@ export class CosmosAPI {
protected defaultEndpoint: string;
private version: string;
private chainInstance: cosmosBase;
private _cosmosSDKVersion: Promise<string> | null = null;
private get cosmosSDKVersion(): Promise<string> {
if (!this._cosmosSDKVersion) {
this._cosmosSDKVersion = this.getCosmosSDKVersion();
}
return this._cosmosSDKVersion;
}

constructor(currencyId: string) {
const crypto = cryptoFactory(currencyId);
Expand Down Expand Up @@ -85,6 +93,12 @@ export class CosmosAPI {
}
};

private getCosmosSDKVersion = async (): Promise<string> => {
const { application_version } = await this.getNodeInfo();
hedi-edelbloute marked this conversation as resolved.
Show resolved Hide resolved
const cosmosSDKVersion = application_version.cosmos_sdk_version;
return cosmosSDKVersion;
};

/**
* @sdk https://docs.cosmos.network/api#tag/Query/operation/Account
* @warning return is technically "any" based on documentation and may differ depending on the chain
Expand Down Expand Up @@ -138,15 +152,14 @@ export class CosmosAPI {
* @sdk https://docs.cosmos.network/api#tag/Service/operation/GetNodeInfo
* @notice returns { application_versoin: { ..., cosmos_sdk_version } } (Since: cosmos-sdk 0.43)
*/
getChainId = async (): Promise<string> => {
const {
data: { default_node_info: defaultNodeInfo },
} = await network<CosmosSDKTypes.GetNodeInfosSDK>({
method: "GET",
url: `${this.defaultEndpoint}/cosmos/base/tendermint/${this.version}/node_info`,
});

return defaultNodeInfo.network;
getNodeInfo = async (): Promise<CosmosSDKTypes.GetNodeInfosSDK> => {
const data = (
await network<CosmosSDKTypes.GetNodeInfosSDK>({
method: "GET",
url: `${this.defaultEndpoint}/cosmos/base/tendermint/${this.version}/node_info`,
})
).data;
return data;
};

/**
Expand Down Expand Up @@ -403,14 +416,20 @@ export class CosmosAPI {
txs: CosmosTx[];
total: number;
}> {
let cosmosSDKVersion = await this.cosmosSDKVersion;
cosmosSDKVersion = semver.coerce(cosmosSDKVersion).version;
let queryparam = "events";
if (semver.gte(cosmosSDKVersion, "0.50.0")) {
queryparam = "query";
}
let serializedOptions = "";
for (const key of Object.keys(options)) {
serializedOptions += options[key] != null ? `&${key}=${options[key]}` : "";
}
const { data } = await network<CosmosSDKTypes.GetTxsEvents>({
method: "GET",
url:
`${nodeUrl}/cosmos/tx/${this.version}/txs?events=` +
`${nodeUrl}/cosmos/tx/${this.version}/txs?${queryparam}=` +
encodeURI(`${filterOn}='${address}'`) +
serializedOptions,
});
Expand All @@ -423,7 +442,7 @@ export class CosmosAPI {

/**
* @sdk https://docs.cosmos.network/api#tag/Service/operation/BroadcastTx
* @depreacted body {..., mode } -> BROADCAST_MODE_BLOCK (Deprecated: post v0.47 use BROADCAST_MODE_SYNC instead)
* @deprecated body {..., mode } -> BROADCAST_MODE_BLOCK (Deprecated: post v0.47 use BROADCAST_MODE_SYNC instead)
* @notice returns {..., events } (Since: cosmos-sdk 0.42.11, 0.44.5, 0.45)
*/
broadcast = async ({ signedOperation: { operation, signature } }): Promise<Operation> => {
Expand Down
19 changes: 12 additions & 7 deletions libs/ledger-live-common/src/families/cosmos/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { CosmosMessage } from "./types";
interface CosmosEventMessage {
type: string;
[key: string]: any;
}

export const getMainMessage = (messages: CosmosMessage[]): CosmosMessage => {
export const getMainMessage = (messages: CosmosEventMessage[]): CosmosEventMessage => {
const messagePriorities: string[] = [
"unbond",
"redelegate",
"delegate",
"withdraw_rewards",
"transfer",
"MsgUndelegate",
"MsgBeginRedelegate",
"MsgDelegate",
"MsgWithdrawDelegatorReward",
"MsgTransfer",
"MsgRecvPacket",
"MsgSend",
];
Comment on lines +6 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘ πŸ‘

const sortedTypes = messages
.filter(m => messagePriorities.includes(m.type))
Expand Down
22 changes: 11 additions & 11 deletions libs/ledger-live-common/src/families/cosmos/helpers.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,42 @@ describe("getMainMessage", () => {
it("should return delegate message with delegate and reward messages (claim rewards, compound)", () => {
const exec = getMainMessage([
{
type: "delegate",
type: "MsgDelegate",
attributes: [],
},
{
type: "withdraw_rewards",
type: "MsgWithdrawDelegatorReward",
attributes: [],
},
]);
expect(exec.type).toEqual("delegate");
expect(exec.type).toEqual("MsgDelegate");
});

it("should return unbond message with unbound and transfer messages", () => {
expect(
getMainMessage([
{
type: "unbond",
type: "MsgUndelegate",
attributes: [],
},
{
type: "transfer",
type: "MsgSend",
attributes: [],
},
]).type,
).toEqual("unbond");
).toEqual("MsgUndelegate");
});

it("should return first transfer message with multiple transfer messages", () => {
const firstTransfer = {
type: "transfer",
type: "MsgSend",
attributes: [],
};
expect(
getMainMessage([
firstTransfer,
{
type: "transfer",
type: "MsgSend",
attributes: [],
},
]),
Expand All @@ -51,15 +51,15 @@ describe("getMainMessage", () => {
expect(
getMainMessage([
{
type: "delegate",
type: "MsgDelegate",
attributes: [],
},
{
type: "redelegate",
type: "MsgBeginRedelegate",
attributes: [],
},
]).type,
).toEqual("redelegate");
).toEqual("MsgBeginRedelegate");
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const signOperation: SignOperationFnSignature<Transaction> = ({ account, deviceI
// Note:
// Cosmos Nano App sign data in Amino way only, not Protobuf.
// This is a legacy outdated standard and a long-term blocking point.
const chainId = await cosmosAPI.getChainId();
const chainId = (await cosmosAPI.getNodeInfo()).default_node_info.network;
const signDoc = makeSignDoc(
aminoMsgs,
feeToEncode,
Expand Down
119 changes: 78 additions & 41 deletions libs/ledger-live-common/src/families/cosmos/js-synchronisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { encodeAccountId } from "../../account";
import { CosmosAPI } from "./api/Cosmos";
import { encodeOperationId } from "../../operation";
import { CosmosOperation, CosmosMessage, CosmosTx } from "./types";
import { CosmosOperation, CosmosTx } from "./types";
import type { OperationType } from "@ledgerhq/types-live";
import { getMainMessage } from "./helpers";
import { parseAmountStringToNumber } from "./logic";
Expand Down Expand Up @@ -52,47 +52,89 @@ const txToOps = (info: AccountShapeInfo, accountId: string, txs: CosmosTx[]): Co

op.hasFailed = tx.code !== 0;

const messages: CosmosMessage[] = op.hasFailed
? tx.events
: tx.logs.map(log => log.events).flat(1);

// simplify the message types
const messages = tx.tx.body.messages.map(message => ({
...message,
type: message["@type"].substring(message["@type"].lastIndexOf(".") + 1),
}));
const mainMessage = getMainMessage(messages);

if (mainMessage === undefined) {
if (!mainMessage) {
// happens when we don't know this message type in our implementation, example : proposal_vote
continue;
}

const correspondingMessages = messages.filter(m => m.type === mainMessage.type);

switch (mainMessage.type) {
case "transfer":
// TODO: handle IBC transfers here
case "MsgTransfer": {
//IBC send
for (const message of correspondingMessages) {
const amount = message.attributes.find(attr => attr.key === "amount")?.value;
const sender = message.attributes.find(attr => attr.key === "sender")?.value;
const recipient = message.attributes.find(attr => attr.key === "recipient")?.value;
if (amount && sender && recipient && amount.endsWith(unitCode)) {
const amount = message["token"].amount;
const denom = message["token"].denom;
const sender = message["sender"];
const recipient = message["receiver"];
if (!amount || !sender || !recipient || !denom || denom !== unitCode) {
continue;
}
if (sender === address) {
if (op.senders.indexOf(sender) === -1) op.senders.push(sender);
if (op.recipients.indexOf(recipient) === -1) op.recipients.push(recipient);
op.value = op.value.plus(parseAmountStringToNumber(amount, unitCode));
if (sender === address) {
op.type = "OUT";
} else if (recipient === address) {
op.value = op.value.plus(new BigNumber(amount));
op.type = "OUT";
}
}
if (op.type === "OUT") {
op.value = op.value.plus(fees);
}
break;
}
case "MsgRecvPacket": {
//IBC receive
for (const message of tx.events) {
if (message.type === "fungible_token_packet") {
const sender = message.attributes.find(attr => attr.key === "sender")?.value;
const receiver = message.attributes.find(attr => attr.key === "receiver")?.value;
const amount = message.attributes.find(attr => attr.key === "amount")?.value;
const denom = message.attributes.find(attr => attr.key === "denom")?.value;
if (sender && receiver === address && amount && denom && denom.endsWith(unitCode)) {
if (op.senders.indexOf(sender) === -1) op.senders.push(sender);
if (op.recipients.indexOf(receiver) === -1) op.recipients.push(receiver);
const amountString = parseAmountStringToNumber(amount, unitCode);
op.value = op.value.plus(new BigNumber(amountString));
op.type = "IN";
}
}
}
break;
}
case "MsgSend": {
for (const message of correspondingMessages) {
const amount = message["amount"].find(amount => amount.denom === unitCode);
const sender = message["from_address"];
const recipient = message["to_address"];
if (!amount || !sender || !recipient) {
continue;
}
if (op.senders.indexOf(sender) === -1) op.senders.push(sender);
if (op.recipients.indexOf(recipient) === -1) op.recipients.push(recipient);
op.value = op.value.plus(amount.amount);
if (sender === address) {
op.type = "OUT";
} else if (recipient === address) {
op.type = "IN";
}
}
if (op.type === "OUT") {
op.value = op.value.plus(fees);
}
break;

case "withdraw_rewards": {
}
case "MsgWithdrawDelegatorReward": {
op.type = "REWARD";
const rewardShards: { amount: BigNumber; address: string }[] = [];
let txRewardValue = new BigNumber(0);
for (const message of correspondingMessages) {
for (const message of tx.events) {
const validator = message.attributes.find(attr => attr.key === "validator")?.value;
const amount = message.attributes.find(attr => attr.key === "amount")?.value;
if (validator && amount && amount.endsWith(unitCode)) {
Expand All @@ -108,54 +150,53 @@ const txToOps = (info: AccountShapeInfo, accountId: string, txs: CosmosTx[]): Co
op.extra.validators = rewardShards;
break;
}
case "delegate": {
case "MsgDelegate": {
op.type = "DELEGATE";
op.value = new BigNumber(fees);
const delegateShards: { amount: BigNumber; address: string }[] = [];
for (const message of correspondingMessages) {
const amount = message.attributes.find(attr => attr.key === "amount")?.value;
const validator = message.attributes.find(attr => attr.key === "validator")?.value;
if (amount && validator && amount.endsWith(unitCode)) {
const amount = message.amount;
const validator = message["validator_address"];
const delegator = message["delegator_address"];
if (amount && validator && amount.denom === unitCode && delegator === address) {
delegateShards.push({
amount: new BigNumber(parseAmountStringToNumber(amount, unitCode)),
amount: new BigNumber(amount.amount),
address: validator,
});
}
}
op.extra.validators = delegateShards;
break;
}
case "redelegate": {
case "MsgBeginRedelegate": {
op.type = "REDELEGATE";
op.value = new BigNumber(fees);
const redelegateShards: { amount: BigNumber; address: string }[] = [];
for (const message of correspondingMessages) {
const amount = message.attributes.find(attr => attr.key === "amount")?.value;
const validatorDst = message.attributes.find(attr => attr.key === "destination_validator")
?.value;
const validatorSrc = message.attributes.find(attr => attr.key === "source_validator")
?.value;
if (amount && validatorDst && validatorSrc && amount.endsWith(unitCode)) {
const amount = message["amount"];
const validatorDst = message["validator_dst_address"];
const validatorSrc = message["validator_src_address"];
if (amount && validatorDst && validatorSrc && amount.denom === unitCode) {
op.extra.sourceValidator = validatorSrc;
redelegateShards.push({
amount: new BigNumber(parseAmountStringToNumber(amount, unitCode)),
amount: new BigNumber(amount.amount),
address: validatorDst,
});
}
}
op.extra.validators = redelegateShards;
break;
}
case "unbond": {
case "MsgUndelegate": {
op.type = "UNDELEGATE";
op.value = new BigNumber(fees);
const unbondShards: { amount: BigNumber; address: string }[] = [];
for (const message of correspondingMessages) {
const amount = message.attributes.find(attr => attr.key === "amount")?.value;
const validator = message.attributes.find(attr => attr.key === "validator")?.value;
if (amount && validator && amount.endsWith(unitCode)) {
const amount = message["amount"];
const validator = message["validator_address"];
if (amount && validator && amount.denom === unitCode) {
unbondShards.push({
amount: new BigNumber(parseAmountStringToNumber(amount, unitCode)),
amount: new BigNumber(amount.amount),
address: validator,
});
}
Expand All @@ -165,10 +206,6 @@ const txToOps = (info: AccountShapeInfo, accountId: string, txs: CosmosTx[]): Co
}
}

if (op.hasFailed) {
op.value = fees;
}

if (tx.tx.body.memo != null) {
op.extra.memo = tx.tx.body.memo;
}
Expand Down