Merge pull request #1388 from cosmos/amino-escaping

Amino JSON string escaping (v2)
This commit is contained in:
Simon Warta 2023-03-13 15:15:46 +01:00 committed by GitHub
commit 41f07c1575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 3 deletions

View File

@ -6,6 +6,14 @@ and this project adheres to
## [Unreleased]
### Fixed
- @cosmjs/amino: Fix escaping of "&", "<" and ">" characters in Amino JSON
encoding to match the Go implementation ([#1373], [#1388]).
[#1373]: https://github.com/cosmos/cosmjs/pull/1373
[#1388]: https://github.com/cosmos/cosmjs/pull/1388
## [0.30.0] - 2023-03-09
### Changed

View File

@ -2,7 +2,7 @@
import { Random } from "@cosmjs/crypto";
import { toBech32 } 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,43 @@ describe("encoding", () => {
});
});
});
describe("escapeCharacters", () => {
it("works", () => {
// Unchanged originals
expect(escapeCharacters(`""`)).toEqual(`""`);
expect(escapeCharacters(`{}`)).toEqual(`{}`);
expect(escapeCharacters(`[]`)).toEqual(`[]`);
expect(escapeCharacters(`[123,null,"foo",[{}]]`)).toEqual(`[123,null,"foo",[{}]]`);
expect(escapeCharacters(`{"num":123}`)).toEqual(`{"num":123}`);
expect(escapeCharacters(`{"memo":"123"}`)).toEqual(`{"memo":"123"}`);
expect(escapeCharacters(`{"memo":"\\u0026"}`)).toEqual(`{"memo":"\\u0026"}`);
// Escapes one
expect(escapeCharacters(`{"m":"with amp: &"}`)).toEqual(`{"m":"with amp: \\u0026"}`);
expect(escapeCharacters(`{"m":"with lt: <"}`)).toEqual(`{"m":"with lt: \\u003c"}`);
expect(escapeCharacters(`{"m":"with gt: >"}`)).toEqual(`{"m":"with gt: \\u003e"}`);
// Escapes multiple
expect(escapeCharacters(`{"m":"with amp: &&"}`)).toEqual(`{"m":"with amp: \\u0026\\u0026"}`);
expect(escapeCharacters(`{"m":"with lt: <<"}`)).toEqual(`{"m":"with lt: \\u003c\\u003c"}`);
expect(escapeCharacters(`{"m":"with gt: >>"}`)).toEqual(`{"m":"with gt: \\u003e\\u003e"}`);
expect(escapeCharacters(`{"m":"with all: &<>"}`)).toEqual(`{"m":"with all: \\u0026\\u003c\\u003e"}`);
});
it("escaped encoding can be decoded to the same document", () => {
const docs = [
{ memo: "ampersand:&,lt:<,gt:>", value: 123.421 },
"",
123,
["foo", "ampersand:&,lt:<,gt:>"],
];
for (const doc of docs) {
const normalEncoding = JSON.stringify(doc);
const escapedEncoding = escapeCharacters(normalEncoding);
expect(JSON.parse(escapedEncoding)).toEqual(JSON.parse(normalEncoding));
expect(JSON.parse(escapedEncoding)).toEqual(doc);
}
});
});
});

View File

@ -72,6 +72,28 @@ export function makeSignDoc(
};
}
export function serializeSignDoc(signDoc: StdSignDoc): Uint8Array {
return toUtf8(sortedJsonStringify(signDoc));
/**
* Takes a valid JSON document and performs the following escapings in string values:
*
* `&` -> `\u0026`
* `<` -> `\u003c`
* `>` -> `\u003e`
*
* Since those characters do not occur in other places of the JSON document, only
* string values are affected.
*
* If the input is invalid JSON, the behaviour is undefined.
*/
export function escapeCharacters(input: string): string {
// When we migrate to target es2021 or above, we can use replaceAll instead of global patterns.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
const amp = /&/g;
const lt = /</g;
const gt = />/g;
return input.replace(amp, "\\u0026").replace(lt, "\\u003c").replace(gt, "\\u003e");
}
export function serializeSignDoc(signDoc: StdSignDoc): Uint8Array {
const serialized = escapeCharacters(sortedJsonStringify(signDoc));
return toUtf8(serialized);
}

View File

@ -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);