diff --git a/CHANGELOG.md b/CHANGELOG.md index 932c35eb9..9baab85bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Improvements - [#750](https://github.com/alleslabs/celatone-frontend/pull/750) api v1 - recent codes list +- [#752](https://github.com/alleslabs/celatone-frontend/pull/752) Support contract state's key as base64 ### Bug fixes diff --git a/src/lib/utils/contractState.test.ts b/src/lib/utils/contractState.test.ts index 63b194b19..51208928c 100644 --- a/src/lib/utils/contractState.test.ts +++ b/src/lib/utils/contractState.test.ts @@ -80,8 +80,28 @@ describe("parseStateKey", () => { }); }); - it("should return a singleton key if parsing fails", () => { - const key = "not hex"; - expect(parseStateKey(key)).toEqual({ type: "singleton", value: key }); + it("should parse base64 key", () => { + const key = "SGVsbG8="; // "Hello" + expect(parseStateKey(key)).toEqual({ type: "singleton", value: "Hello" }); + }); + + it("should parse base64 key with multiple values", () => { + const key = + "AAdhdWN0aW9uc2VpMTJuZTdxdG1kd2QwajAzdDl0NWVzOG1kNjZ3cTRlNXhnOW5lbGFkcnNhZzhmeDN5ODlyY3M1bTJ4YWpDOTgxWjhvUVZUZXBPSzRQclN1c2VpMXhzZjA4bGtoNzBjeW10aGY0Z2h4YWVmODR3NHVsZTd5bnFucXJr=="; + expect(parseStateKey(key)).toEqual({ + type: "bucket", + values: [ + "auction", + "sei12ne7qtmdwd0j03t9t5es8md66wq4e5xg9neladrsag8fx3y89rcs5m2xajC981Z8oQVTepOK4PrSusei1xsf08lkh70cymthf4ghxaef84w4ule7ynqnqrk", + ], + }); + }); + + it("should parse base64 key name length less than 4", () => { + const key = "AAZ0b2tlbnMx"; // 0006746f6b656e7331 + expect(parseStateKey(key)).toEqual({ + type: "bucket", + values: ["tokens", "1"], + }); }); }); diff --git a/src/lib/utils/contractState.ts b/src/lib/utils/contractState.ts index 0f513685f..51401a8d3 100644 --- a/src/lib/utils/contractState.ts +++ b/src/lib/utils/contractState.ts @@ -1,13 +1,18 @@ import type { ContractState, DecodedKey } from "lib/types"; +import { isHex } from "./validate"; + const nameRegex = /^[a-zA-Z0-9_{}:"'/\\,\\[\]()]+$/; export const hexToString = (hex: string) => Buffer.from(hex, "hex").toString("utf-8"); -export const parseStateKey = (key: string): DecodedKey => { +// eslint-disable-next-line sonarjs/cognitive-complexity +export const parseStateKey = (rawKey: string): DecodedKey => { try { - const decodedStr = hexToString(key); + const decodedStr = isHex(rawKey) + ? hexToString(rawKey) + : Buffer.from(rawKey, "base64").toString(); if (decodedStr === "") throw new Error("Invalid hex string for decoding"); if (nameRegex.test(decodedStr)) { return { @@ -16,6 +21,9 @@ export const parseStateKey = (key: string): DecodedKey => { }; } + const key = isHex(rawKey) + ? rawKey + : Buffer.from(rawKey, "base64").toString("hex"); const values: string[] = []; let currentIndex = 0; while (currentIndex < key.length) { @@ -24,7 +32,8 @@ export const parseStateKey = (key: string): DecodedKey => { // We've assumed that the length of the key is less than 256 // This should be the last part of key - if (!(length > 0 && length <= 256)) { + const remainingLength = key.length - currentIndex; + if (!(length > 0 && length <= 256) || remainingLength <= 4) { const valueHex = key.slice(currentIndex); const decodedValue = hexToString(valueHex); values.push(nameRegex.test(decodedValue) ? decodedValue : valueHex); @@ -60,7 +69,7 @@ export const parseStateKey = (key: string): DecodedKey => { return { type: "singleton", - value: key, + value: rawKey, }; }; diff --git a/src/lib/utils/validate.test.ts b/src/lib/utils/validate.test.ts index b97b14b80..6e48de826 100644 --- a/src/lib/utils/validate.test.ts +++ b/src/lib/utils/validate.test.ts @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { isId, isTxHash, isPosDecimal } from "./validate"; +import { isId, isTxHash, isPosDecimal, isHex } from "./validate"; describe("isId", () => { test("valid", () => { @@ -24,6 +24,20 @@ describe("isId", () => { }); }); +describe("isHex", () => { + test("valid", () => { + expect(isHex("1234ABCD")).toBeTruthy(); + }); + describe("invalid", () => { + test("empty string", () => { + expect(isHex("")).toBeFalsy(); + }); + test("non-hexstring", () => { + expect(isHex("XYZ")).toBeFalsy(); + }); + }); +}); + describe("isTxHash", () => { test("valid", () => { expect( diff --git a/src/lib/utils/validate.ts b/src/lib/utils/validate.ts index 8d6da4adf..537186f88 100644 --- a/src/lib/utils/validate.ts +++ b/src/lib/utils/validate.ts @@ -14,15 +14,19 @@ export const isPosDecimal = (input: string): boolean => { export const isId = (input: string): boolean => input.length <= 7 && isPosDecimal(input); -export const isTxHash = (input: string): boolean => { +export const isHex = (input: string): boolean => { + if (input.trim() === "") return false; try { fromHex(input); } catch { return false; } - return input.length === 64; + return true; }; +export const isTxHash = (input: string): boolean => + isHex(input) && input.length === 64; + const isHexAddress = (address: string, length: number): boolean => { const regex = new RegExp(`^0x[a-fA-F0-9]{1,${length}}$`); if (!regex.test(address)) { @@ -30,12 +34,7 @@ const isHexAddress = (address: string, length: number): boolean => { } const strip = padHexAddress(address as HexAddr, length).slice(2); - try { - fromHex(strip); - } catch { - return false; - } - return true; + return isHex(strip); }; export const isHexWalletAddress = (address: string) =>