diff --git a/packages/amino/src/signdoc.spec.ts b/packages/amino/src/signdoc.spec.ts index 121b35879a..0ae5075d4b 100644 --- a/packages/amino/src/signdoc.spec.ts +++ b/packages/amino/src/signdoc.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Random } from "@cosmjs/crypto"; -import { toBech32 } from "@cosmjs/encoding"; +import { fromUtf8, toBech32, toUtf8 } from "@cosmjs/encoding"; -import { AminoMsg, makeSignDoc, sortedJsonStringify } from "./signdoc"; +import { AminoMsg, escapeCharacters, makeSignDoc, sortedJsonStringify } from "./signdoc"; function makeRandomAddress(): string { return toBech32("cosmos", Random.getBytes(20)); @@ -132,4 +132,14 @@ describe("encoding", () => { }); }); }); + + describe("escape characters after utf8 encoding", () => { + it("works", () => { + const test = JSON.stringify({ memo: "ampersand:&,lt:<,gt:>" }); + const utf8Encoding = toUtf8(test); + const escapedEncoding = escapeCharacters(utf8Encoding); + + expect(JSON.parse(fromUtf8(utf8Encoding))).toEqual(JSON.parse(fromUtf8(escapedEncoding))); + }); + }); }); diff --git a/packages/amino/src/signdoc.ts b/packages/amino/src/signdoc.ts index 2348353eb3..d441707640 100644 --- a/packages/amino/src/signdoc.ts +++ b/packages/amino/src/signdoc.ts @@ -72,6 +72,35 @@ export function makeSignDoc( }; } +export function escapeCharacters(encodedArray: Uint8Array): Uint8Array { + const AmpersandUnicode = new Uint8Array([92, 117, 48, 48, 50, 54]); + const LtSignUnicode = new Uint8Array([92, 117, 48, 48, 51, 99]); + const GtSignUnicode = new Uint8Array([92, 117, 48, 48, 51, 101]); + + const AmpersandAscii = 38; + const LtSign = 60; // < + const GtSign = 62; // > + + const filteredIndex: number[] = []; + encodedArray.forEach((value, index) => { + if (value === AmpersandAscii || value === LtSign || value === GtSign) filteredIndex.push(index); + }); + + let result = new Uint8Array([...encodedArray]); + const reversedFilteredIndex = filteredIndex.reverse(); + reversedFilteredIndex.forEach((value) => { + let unicode = AmpersandUnicode; + if (result[value] === LtSign) { + unicode = LtSignUnicode; + } else if (result[value] === GtSign) { + unicode = GtSignUnicode; + } + result = new Uint8Array([...result.slice(0, value), ...unicode, ...result.slice(value + 1)]); + }); + + return result; +} + export function serializeSignDoc(signDoc: StdSignDoc): Uint8Array { - return toUtf8(sortedJsonStringify(signDoc)); + return escapeCharacters(toUtf8(sortedJsonStringify(signDoc))); } diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index e0a58e2b8e..3f15b5c9b3 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -455,6 +455,33 @@ describe("SigningStargateClient", () => { }); describe("legacy Amino mode", () => { + it("works with special characters in memo", async () => { + pendingWithoutSimapp(); + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const msgSend: MsgSend = { + fromAddress: faucet.address0, + toAddress: makeRandomAddress(), + amount: coins(1234, "ucosm"), + }; + const msgAny: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const fee = { + amount: coins(2000, "ucosm"), + gas: "200000", + }; + const memo = "ampersand:&,lt:<,gt:>"; + const result = await client.signAndBroadcast(faucet.address0, [msgAny], fee, memo); + assertIsDeliverTxSuccess(result); + }); + it("works with bank MsgSend", async () => { pendingWithoutSimapp(); const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic);