diff --git a/packages/oraidex-common/package.json b/packages/oraidex-common/package.json index f10074ff..dd28de7f 100644 --- a/packages/oraidex-common/package.json +++ b/packages/oraidex-common/package.json @@ -1,6 +1,6 @@ { "name": "@oraichain/oraidex-common", - "version": "1.0.74", + "version": "1.0.75", "main": "build/index.js", "files": [ "build/" diff --git a/packages/oraidex-common/src/helper.ts b/packages/oraidex-common/src/helper.ts index cf442ed7..89f64233 100644 --- a/packages/oraidex-common/src/helper.ts +++ b/packages/oraidex-common/src/helper.ts @@ -1,7 +1,7 @@ -import { ExecuteInstruction, toBinary } from "@cosmjs/cosmwasm-stargate"; -import { toUtf8 } from "@cosmjs/encoding"; -import { Coin, EncodeObject } from "@cosmjs/proto-signing"; -import { Event } from "@cosmjs/tendermint-rpc/build/tendermint37"; +import { ExecuteInstruction, JsonObject, fromBinary, toBinary, wasmTypes } from "@cosmjs/cosmwasm-stargate"; +import { fromAscii, toUtf8 } from "@cosmjs/encoding"; +import { Coin, EncodeObject, Registry, decodeTxRaw } from "@cosmjs/proto-signing"; +import { Event, Attribute } from "@cosmjs/tendermint-rpc/build/tendermint37"; import { AssetInfo, Uint128 } from "@oraichain/oraidex-contracts-sdk"; import { TokenInfoResponse } from "@oraichain/oraidex-contracts-sdk/build/OraiswapToken.types"; import bech32 from "bech32"; @@ -31,6 +31,8 @@ import { } from "./token"; import { StargateMsg, Tx } from "./tx"; import { BigDecimal } from "./bigdecimal"; +import { TextProposal } from "cosmjs-types/cosmos/gov/v1beta1/gov"; +import { defaultRegistryTypes as defaultStargateTypes, IndexedTx, logs, StargateClient } from "@cosmjs/stargate"; export const getEvmAddress = (bech32Address: string) => { if (!bech32Address) throw new Error("bech32 address is empty"); @@ -433,3 +435,57 @@ export function parseAssetInfoOnlyDenom(info: AssetInfo): string { if ("native_token" in info) return info.native_token.denom; return info.token.contract_addr; } + +export const decodeProto = (value: JsonObject) => { + if (!value) throw "value is not defined"; + + const typeUrl = value.type_url || value.typeUrl; + if (typeUrl) { + const customRegistry = new Registry([...defaultStargateTypes, ...wasmTypes]); + customRegistry.register("/cosmos.gov.v1beta1.TextProposal", TextProposal); + // decode proto + return decodeProto(customRegistry.decode({ typeUrl, value: value.value })); + } + + for (const k in value) { + if (typeof value[k] === "string") { + try { + value[k] = fromBinary(value[k]); + } catch {} + } + if (typeof value[k] === "object") value[k] = decodeProto(value[k]); + } + if (value.msg instanceof Uint8Array) value.msg = JSON.parse(fromAscii(value.msg)); + return value; +}; + +export const parseWasmEvents = (events: readonly Event[]): { [key: string]: string }[] => { + const wasmEvents = events.filter((e) => e.type.startsWith("wasm")); + const attrs: { [key: string]: string }[] = []; + for (const wasmEvent of wasmEvents) { + let attr: { [key: string]: string }; + for (const { key, value } of wasmEvent.attributes) { + if (key === "_contract_address") { + if (attr) attrs.push(attr); + attr = {}; + } + attr[key] = value; + } + attrs.push(attr); + } + return attrs; +}; + +export const parseTxToMsgsAndEvents = (indexedTx: Tx, eventsParser?: (events: readonly Event[]) => Attribute[]) => { + if (!indexedTx) return []; + const { rawLog, tx } = indexedTx; + const { body } = decodeTxRaw(tx); + const messages = body.messages.map(decodeProto); + const logs: logs.Log[] = JSON.parse(rawLog); + + return logs.map((log) => { + const index = log.msg_index ?? 0; + const attrs = eventsParser ? eventsParser(log.events) : parseWasmEvents(log.events); + return { attrs, message: messages[index] }; + }); +}; diff --git a/packages/oraidex-common/tests/helper.spec.ts b/packages/oraidex-common/tests/helper.spec.ts index 109b8690..13273133 100644 --- a/packages/oraidex-common/tests/helper.spec.ts +++ b/packages/oraidex-common/tests/helper.spec.ts @@ -1,20 +1,17 @@ -import { - AIRI_CONTRACT, - AVERAGE_COSMOS_GAS_PRICE, - MILKYBSC_ORAICHAIN_DENOM, - MILKY_CONTRACT, - ORAI, - USDC_CONTRACT, - USDT_CONTRACT -} from "../src/constant"; -import { AmountDetails, TokenItemType, cosmosTokens, flattenTokens, oraichainTokens } from "../src/token"; +import { Coin } from "@cosmjs/amino"; +import { toBinary } from "@cosmjs/cosmwasm-stargate"; +import { StargateClient } from "@cosmjs/stargate"; +import { Event } from "@cosmjs/tendermint-rpc/build/tendermint37"; +import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { AIRI_CONTRACT, AVERAGE_COSMOS_GAS_PRICE, MILKYBSC_ORAICHAIN_DENOM, ORAI } from "../src/constant"; import { calculateMinReceive, calculateTimeoutTimestamp, + decodeProto, ethToTronAddress, findToTokenOnOraiBridge, - getEvmAddress, getCosmosGasPrice, + getEvmAddress, getSubAmountDetails, getTokenOnOraichain, getTokenOnSpecificChainId, @@ -24,6 +21,8 @@ import { parseAssetInfo, parseTokenInfo, parseTokenInfoRawDenom, + parseTxToMsgsAndEvents, + parseWasmEvents, toAmount, toAssetInfo, toDecimal, @@ -32,11 +31,9 @@ import { tronToEthAddress, validateNumber } from "../src/helper"; -import { CoinGeckoId, NetworkChainId, OraiToken } from "../src/network"; -import { AssetInfo } from "@oraichain/oraidex-contracts-sdk"; +import { CoinGeckoId, NetworkChainId } from "../src/network"; import { isFactoryV1 } from "../src/pairs"; -import { Coin } from "@cosmjs/amino"; -import { toBinary } from "@cosmjs/cosmwasm-stargate"; +import { AmountDetails, TokenItemType, cosmosTokens, flattenTokens, oraichainTokens } from "../src/token"; describe("should helper functions in helper run exactly", () => { const amounts: AmountDetails = { @@ -393,4 +390,164 @@ describe("should helper functions in helper run exactly", () => { expect(getCosmosGasPrice({ low: 0, average: 0, high: 0 })).toEqual(0); expect(getCosmosGasPrice()).toEqual(AVERAGE_COSMOS_GAS_PRICE); }); + + // TODO: add more tests for this func + it("test-parseTxToMsgsAndEvents", async () => { + // case 1: undefined input + const reuslt = parseTxToMsgsAndEvents(undefined as any); + expect(reuslt).toEqual([]); + + // case 2: real tx with multiple msgs and multiple contract calls + const client = await StargateClient.connect("wss://rpc.orai.io"); + const indexedTx = await client.getTx("9B435E4014DEBA5AB80D4BB8F52D766A6C14BFCAC21F821CDB96F4ABB4E29B17"); + client.disconnect(); + + const data = parseTxToMsgsAndEvents(indexedTx!); + expect(data.length).toEqual(2); + expect(data[0].message).toMatchObject({ + sender: "orai16hv74w3eu3ek0muqpgp4fekhrqgpzl3hd3qeqk", + contract: "orai1nt58gcu4e63v7k55phnr3gaym9tvk3q4apqzqccjuwppgjuyjy6sxk8yzp", + msg: { + execute_order_book_pair: { + asset_infos: [ + { + token: { + contract_addr: "orai1lplapmgqnelqn253stz6kmvm3ulgdaytn89a8mz9y85xq8wd684s6xl3lt" + } + }, + { + token: { + contract_addr: "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + } + ], + limit: 100 + } + }, + funds: [] + }); + expect(data[0].attrs.length).toEqual(5); + expect(data[1].message).toMatchObject({ + sender: "orai16hv74w3eu3ek0muqpgp4fekhrqgpzl3hd3qeqk", + contract: "orai1nt58gcu4e63v7k55phnr3gaym9tvk3q4apqzqccjuwppgjuyjy6sxk8yzp", + msg: { + execute_order_book_pair: { + asset_infos: [ + { native_token: { denom: "orai" } }, + { + token: { + contract_addr: "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + } + ], + limit: 100 + } + }, + funds: [] + }); + expect(data[0].attrs.length).toEqual(5); + }, 20000); + + it("test-decodeProto-with-value-input-undefined", () => { + expect(() => decodeProto(undefined)).toThrow("value is not defined"); + }); + + it.each([ + [ + // case 1: value with type_url and valid value + { + type_url: "/cosmos.gov.v1beta1.TextProposal", + value: Uint8Array.from([10, 3, 97, 98, 99]) // Example byte array + }, + { title: "abc", description: "" } + ], + + [ + // case 2: value with typeUrl and valid value + { + type_url: "/cosmos.gov.v1beta1.TextProposal", + value: Uint8Array.from([10, 3, 97, 98, 99]) + }, + { title: "abc", description: "" } + ], + + // case 3: value is object with binary string and object properties is binary string + [ + { + key1: "InZhbHVlMSI=", + key2: { + nestedKey: "Im5lc3RlZC1zdHJpbmctdmFsdWUi" + } + }, + { + key1: "value1", + key2: { + nestedKey: "nested-string-value" + } + } + ], + + // case 4: value is object with text string + [ + { + key1: "text-string" + }, + { + key1: "text-string" + } + ], + + // case 5: value.msg is instance of Uint8Array + [ + { + msg: Uint8Array.from([123, 34, 107, 101, 121, 34, 58, 34, 118, 97, 108, 117, 101, 34, 125]) // Uint8Array representation of '{"key": "value"}' + }, + { + msg: { + key: "value" + } + } + ] + ])("test-decodeProto", (value, expectation) => { + // act + const res = decodeProto(value); + + // assertion + expect(res).toEqual(expectation); + }); + + it.each<[string, readonly Event[], { [key: string]: string }[]]>([ + ["empty-events-array", [], []], + ["events-with-single-event-without-attributes", [{ type: "wasmEvent", attributes: [] }], []], + [ + "events-with-single-event-with-attributes", + [ + { + type: "wasmEvent", + attributes: [ + { key: "_contract_address", value: "addr1" }, + { key: "key1", value: "value1" } + ] + } + ], + [{ _contract_address: "addr1", key1: "value1" }] + ], + [ + "events-with-multiple-events-with-and-without-attributes", + [ + { + type: "wasmEvent", + attributes: [ + { key: "_contract_address", value: "addr1" }, + { key: "key2", value: "value2" } + ] + }, + { type: "otherEvent", attributes: [{ key: "key3", value: "value3" }] }, + { type: "wasmEvent", attributes: [{ key: "_contract_address", value: "addr2" }] } + ], + [{ _contract_address: "addr1", key2: "value2" }, { _contract_address: "addr2" }] + ] + ])("test-parseWasmEvents-with-case: %p", (_case, input, expectedOutput) => { + expect(parseWasmEvents(input)).toEqual(expectedOutput); + }); });