diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e4f4d97bf..470eb43f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,9 +45,12 @@ and this project adheres to - @cosmjs/cosmwasm-stargate: Add constructors `CosmWasmClient.create` and `SigningCosmWasmClient.createWithSigner` to construct with a given Tendermint client ([#1376]). +- @cosmjs/cosmwasm-stargate: Add `instantiate2Address` to pre-calculate + addresses for Instantiate2 ([#1253]). - @cosmjs/stargate: Add `txIndex` to `DeliverTxResponse` and `IndexedTx` ([#1361]). +[#1253]: https://github.com/cosmos/cosmjs/pull/1253 [#1308]: https://github.com/cosmos/cosmjs/pull/1308 [#1361]: https://github.com/cosmos/cosmjs/issues/1361 [#1376]: https://github.com/cosmos/cosmjs/pull/1376 diff --git a/packages/cli/examples/instantiate2_addresses.ts b/packages/cli/examples/instantiate2_addresses.ts new file mode 100644 index 0000000000..566c064052 --- /dev/null +++ b/packages/cli/examples/instantiate2_addresses.ts @@ -0,0 +1,62 @@ +import { fromBech32, fromHex, toBech32, toHex, toUtf8 } from "@cosmjs/encoding"; +import { _instantiate2AddressIntermediate } from "@cosmjs/cosmwasm-stargate"; + +function makeTestingAddress(length: number): string { + let data = new Uint8Array(length); + data.fill(0x99, 0); + data.fill(0xaa, 5); + data.fill(0xbb, 10); + data.fill(0xcc, 15); + data.fill(0xdd, 20); + data.fill(0xee, 25); + data.fill(0xff, 30); + return toBech32("purple", data); +} + +let out: Array = []; + +const checksums = [ + fromHex("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5"), + fromHex("1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b"), +]; +const salts = [ + toUtf8("a"), + fromHex( + "AABBCCDDEEFFFFEEDDBBCCDDAA66551155aaaaBBCC787878789900AABBCCDDEEFFFFEEDDBBCCDDAA66551155aaaaBBCC787878789900aabbbbcc221100acadae", + ), +]; +const msgs = [null, JSON.stringify({}), JSON.stringify({ some: 123, structure: { nested: ["ok", true] } })]; + +for (let checksum of checksums) { + for (let creator of [makeTestingAddress(20), makeTestingAddress(32)]) { + for (let salt of salts) { + for (let msg of msgs) { + const { key, addressData, address } = _instantiate2AddressIntermediate( + checksum, + creator, + salt, + msg, + "purple", + ); + out.push({ + in: { + checksum: toHex(checksum), + creator, + creatorData: toHex(fromBech32(creator).data), + salt: toHex(salt), + msg, + }, + intermediate: { + key: toHex(key), + addressData: toHex(addressData), + }, + out: { + address: address, + }, + }); + } + } + } +} + +console.log(JSON.stringify(out, undefined, 2)); diff --git a/packages/cli/run_examples.sh b/packages/cli/run_examples.sh index f4edba233d..c276f1197f 100755 --- a/packages/cli/run_examples.sh +++ b/packages/cli/run_examples.sh @@ -10,6 +10,7 @@ if [ -n "${SIMAPP44_ENABLED:-}" ]; then fi yarn node ./bin/cosmjs-cli --init examples/faucet_addresses.ts --code "process.exit(0)" yarn node ./bin/cosmjs-cli --init examples/generate_address.ts --code "process.exit(0)" +yarn node ./bin/cosmjs-cli --init examples/instantiate2_addresses.ts --code "process.exit(0)" yarn node ./bin/cosmjs-cli --init examples/local_faucet.ts --code "process.exit(0)" yarn node ./bin/cosmjs-cli --init examples/mask.ts --code "process.exit(0)" yarn node ./bin/cosmjs-cli --init examples/multisig_address.ts --code "process.exit(0)" diff --git a/packages/cosmwasm-stargate/src/index.ts b/packages/cosmwasm-stargate/src/index.ts index c41663d936..2db1cb3911 100644 --- a/packages/cosmwasm-stargate/src/index.ts +++ b/packages/cosmwasm-stargate/src/index.ts @@ -1,5 +1,6 @@ export { Code, CodeDetails, Contract, ContractCodeHistoryEntry, CosmWasmClient } from "./cosmwasmclient"; export { fromBinary, toBinary } from "./encoding"; +export { _instantiate2AddressIntermediate, instantiate2Address } from "./instantiate2"; export { cosmWasmTypes, createWasmAminoConverters, diff --git a/packages/cosmwasm-stargate/src/instantiate2.spec.ts b/packages/cosmwasm-stargate/src/instantiate2.spec.ts new file mode 100644 index 0000000000..571346e521 --- /dev/null +++ b/packages/cosmwasm-stargate/src/instantiate2.spec.ts @@ -0,0 +1,53 @@ +import { fromHex } from "@cosmjs/encoding"; + +import { instantiate2Address } from "./instantiate2"; + +describe("instantiate2", () => { + describe("instantiate2Address", () => { + it("works", () => { + // Some entries from https://gist.github.com/webmaster128/e4d401d414bd0e7e6f70482f11877fbe + { + const checksum = fromHex("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5"); + const creator = "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py"; + const salt = fromHex("61"); + expect(instantiate2Address(checksum, creator, salt, "purple")).toEqual( + "purple1t6r960j945lfv8mhl4mage2rg97w63xeynwrupum2s2l7em4lprs9ce5hk", + ); + } + { + const checksum = fromHex("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5"); + const creator = "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py"; + const salt = fromHex( + "aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbccddeeffffeeddbbccddaa66551155aaaabbcc787878789900aabbbbcc221100acadae", + ); + expect(instantiate2Address(checksum, creator, salt, "purple")).toEqual( + "purple1jwzvvfyvpwchrccxl476pxf7c83qawsqv3f2820q0zyrav6eg4jqdcq7gc", + ); + } + { + const checksum = fromHex("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5"); + const creator = "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m"; + const salt = fromHex("61"); + expect(instantiate2Address(checksum, creator, salt, "purple")).toEqual( + "purple1juj7jn6j3k9h35euyhealntquc2zmzlxp2ek76jmtypkl4g4vrdsfwmwxk", + ); + } + { + const checksum = fromHex("13a1fc994cc6d1c81b746ee0c0ff6f90043875e0bf1d9be6b7d779fc978dc2a5"); + const creator = "purple1nxvenxve42424242hwamhwamenxvenxvmhwamhwaamhwamhwlllsatsy6m"; + const salt = fromHex("61"); + expect(instantiate2Address(checksum, creator, salt, "purple")).toEqual( + "purple1juj7jn6j3k9h35euyhealntquc2zmzlxp2ek76jmtypkl4g4vrdsfwmwxk", + ); + } + { + const checksum = fromHex("1da6c16de2cbaf7ad8cbb66f0925ba33f5c278cb2491762d04658c1480ea229b"); + const creator = "purple1nxvenxve42424242hwamhwamenxvenxvhxf2py"; + const salt = fromHex("61"); + expect(instantiate2Address(checksum, creator, salt, "purple")).toEqual( + "purple1h9wyvusc6sy2p7fsgmez0dhvullpsyel7vq38k6d9fa7ehlv59qsvnyh36", + ); + } + }); + }); +}); diff --git a/packages/cosmwasm-stargate/src/instantiate2.ts b/packages/cosmwasm-stargate/src/instantiate2.ts new file mode 100644 index 0000000000..edc7f251eb --- /dev/null +++ b/packages/cosmwasm-stargate/src/instantiate2.ts @@ -0,0 +1,69 @@ +import { Sha256, sha256 } from "@cosmjs/crypto"; +import { fromBech32, toAscii, toBech32, toUtf8 } from "@cosmjs/encoding"; +import { Uint64 } from "@cosmjs/math"; +import { assert } from "@cosmjs/utils"; + +/** + * The "Basic Address" Hash from + * https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/docs/architecture/adr-028-public-key-addresses.md + */ +function hash(type: string, key: Uint8Array): Uint8Array { + return new Sha256(sha256(toAscii(type))).update(key).digest(); +} + +/** + * Takes an integer [0, 2**64-1] and returns a one-byte encoding of it. + */ +function toUint64(int: number): Uint8Array { + return Uint64.fromNumber(int).toBytesBigEndian(); +} + +/** + * Private function to export test vector data for https://github.com/cosmos/cosmjs/pull/1253. + * Do not use in production code. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export function _instantiate2AddressIntermediate( + checksum: Uint8Array, + creator: string, + salt: Uint8Array, + msg: string | null, + prefix: string, +): { key: Uint8Array; addressData: Uint8Array; address: string } { + assert(checksum.length === 32); + const creatorData = fromBech32(creator).data; + + const msgData = typeof msg === "string" ? toUtf8(msg) : new Uint8Array(); + + // Validate inputs + if (salt.length < 1 || salt.length > 64) throw new Error("Salt must be between 1 and 64 bytes"); + + const key = new Uint8Array([ + ...toAscii("wasm"), + 0x00, + ...toUint64(checksum.length), + ...checksum, + ...toUint64(creatorData.length), + ...creatorData, + ...toUint64(salt.length), + ...salt, + ...toUint64(msgData.length), + ...msgData, + ]); + const addressData = hash("module", key); + const address = toBech32(prefix, addressData); + return { key, addressData, address }; +} + +/** + * Predictable address generation for the MsgInstantiateContract2 + * introduced with wasmd 0.29. + */ +export function instantiate2Address( + checksum: Uint8Array, + creator: string, + salt: Uint8Array, + prefix: string, +): string { + return _instantiate2AddressIntermediate(checksum, creator, salt, null, prefix).address; +}