From ef307f53cc5e2b74c9f69c0f60df776e69d8cba1 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 9 Dec 2020 12:40:21 +0000 Subject: [PATCH] cosmwasm-stargate: Add wasm query tests --- .../src/queries/wasm.spec.ts | 447 ++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 packages/cosmwasm-stargate/src/queries/wasm.spec.ts diff --git a/packages/cosmwasm-stargate/src/queries/wasm.spec.ts b/packages/cosmwasm-stargate/src/queries/wasm.spec.ts new file mode 100644 index 00000000..a2696082 --- /dev/null +++ b/packages/cosmwasm-stargate/src/queries/wasm.spec.ts @@ -0,0 +1,447 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { sha256 } from "@cosmjs/crypto"; +import { fromAscii, fromHex, toAscii, toHex } from "@cosmjs/encoding"; +import { Coin, coin, coins, logs, StdFee } from "@cosmjs/launchpad"; +import { DirectSecp256k1HdWallet, OfflineDirectSigner, Registry } from "@cosmjs/proto-signing"; +import { + assertIsBroadcastTxSuccess, + BroadcastTxResponse, + parseRawLog, + SigningStargateClient, +} from "@cosmjs/stargate"; +import { assert } from "@cosmjs/utils"; +import Long from "long"; + +import { cosmwasm } from "../codec"; +import { SigningCosmWasmClient } from "../signingcosmwasmclient"; +import { + alice, + base64Matcher, + bech32AddressMatcher, + ContractUploadInstructions, + getHackatom, + makeRandomAddress, + makeWasmClient, + pendingWithoutWasmd, + wasmd, + wasmdEnabled, +} from "../testutils.spec"; + +const { MsgExecuteContract, MsgInstantiateContract, MsgStoreCode } = cosmwasm.wasm.v1beta1; + +const registry = new Registry([ + ["/cosmwasm.wasm.v1beta1.MsgExecuteContract", MsgExecuteContract], + ["/cosmwasm.wasm.v1beta1.MsgStoreCode", MsgStoreCode], + ["/cosmwasm.wasm.v1beta1.MsgInstantiateContract", MsgInstantiateContract], +]); + +async function uploadContract( + signer: OfflineDirectSigner, + contract: ContractUploadInstructions, +): Promise { + const memo = "My first contract on chain"; + const theMsg = { + typeUrl: "/cosmwasm.wasm.v1beta1.MsgStoreCode", + value: MsgStoreCode.create({ + sender: alice.address0, + wasmByteCode: contract.data, + source: contract.source || "", + builder: contract.builder || "", + }), + }; + const fee: StdFee = { + amount: coins(5000000, "ucosm"), + gas: "89000000", + }; + const firstAddress = (await signer.getAccounts())[0].address; + const client = await SigningStargateClient.connectWithWallet(wasmd.endpoint, signer, { registry }); + return client.signAndBroadcast(firstAddress, [theMsg], fee, memo); +} + +async function instantiateContract( + signer: OfflineDirectSigner, + codeId: number, + beneficiaryAddress: string, + transferAmount?: readonly Coin[], +): Promise { + const memo = "Create an escrow instance"; + const theMsg = { + typeUrl: "/cosmwasm.wasm.v1beta1.MsgInstantiateContract", + value: MsgInstantiateContract.create({ + sender: alice.address0, + codeId: Long.fromNumber(codeId), + label: "my escrow", + initMsg: toAscii( + JSON.stringify({ + verifier: alice.address0, + beneficiary: beneficiaryAddress, + }), + ), + initFunds: transferAmount ? [...transferAmount] : [], + }), + }; + const fee: StdFee = { + amount: coins(5000000, "ucosm"), + gas: "89000000", + }; + + const firstAddress = (await signer.getAccounts())[0].address; + const client = await SigningStargateClient.connectWithWallet(wasmd.endpoint, signer, { registry }); + return client.signAndBroadcast(firstAddress, [theMsg], fee, memo); +} + +async function executeContract( + signer: OfflineDirectSigner, + contractAddress: string, + msg: Record, +): Promise { + const memo = "Time for action"; + const theMsg = { + typeUrl: "/cosmwasm.wasm.v1beta1.MsgExecuteContract", + value: MsgExecuteContract.create({ + sender: alice.address0, + contract: contractAddress, + msg: toAscii(JSON.stringify(msg)), + sentFunds: [], + }), + }; + const fee: StdFee = { + amount: coins(5000000, "ucosm"), + gas: "89000000", + }; + + const firstAddress = (await signer.getAccounts())[0].address; + const client = await SigningCosmWasmClient.connectWithWallet(wasmd.endpoint, signer, { registry }); + return client.signAndBroadcast(firstAddress, [theMsg], fee, memo); +} + +describe("WasmExtension", () => { + const hackatom = getHackatom(); + const hackatomConfigKey = toAscii("config"); + let hackatomCodeId: number | undefined; + let hackatomContractAddress: string | undefined; + + beforeAll(async () => { + if (wasmdEnabled()) { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, "wasm"); + const result = await uploadContract(wallet, hackatom); + assertIsBroadcastTxSuccess(result); + hackatomCodeId = Number.parseInt( + JSON.parse(result.rawLog!)[0].events[0].attributes.find( + (attribute: any) => attribute.key === "code_id", + ).value, + 10, + ); + + const instantiateResult = await instantiateContract(wallet, hackatomCodeId, makeRandomAddress()); + assertIsBroadcastTxSuccess(instantiateResult); + hackatomContractAddress = JSON.parse(instantiateResult.rawLog!)[0] + .events.find((event: any) => event.type === "message") + .attributes.find((attribute: any) => attribute.key === "contract_address").value; + } + }); + + describe("listCodeInfo", () => { + it("has recently uploaded contract as last entry", async () => { + pendingWithoutWasmd(); + assert(hackatomCodeId); + const client = await makeWasmClient(wasmd.endpoint); + const { codeInfos } = await client.unverified.wasm.listCodeInfo(); + assert(codeInfos); + const lastCode = codeInfos[codeInfos.length - 1]; + expect(lastCode.codeId!.toNumber()).toEqual(hackatomCodeId); + expect(lastCode.creator).toEqual(alice.address0); + expect(lastCode.source).toEqual(hackatom.source); + expect(lastCode.builder).toEqual(hackatom.builder); + expect(toHex(lastCode.dataHash!)).toEqual(toHex(sha256(hackatom.data))); + }); + }); + + describe("getCode", () => { + it("contains fill code information", async () => { + pendingWithoutWasmd(); + assert(hackatomCodeId); + const client = await makeWasmClient(wasmd.endpoint); + const { codeInfo, data } = await client.unverified.wasm.getCode(hackatomCodeId); + expect(codeInfo!.codeId!.toNumber()).toEqual(hackatomCodeId); + expect(codeInfo!.creator).toEqual(alice.address0); + expect(codeInfo!.source).toEqual(hackatom.source); + expect(codeInfo!.builder).toEqual(hackatom.builder); + expect(toHex(codeInfo!.dataHash!)).toEqual(toHex(sha256(hackatom.data))); + expect(data).toEqual(hackatom.data); + }); + }); + + // TODO: move listContractsByCodeId tests out of here + describe("getContractInfo", () => { + it("works", async () => { + pendingWithoutWasmd(); + assert(hackatomCodeId); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, "wasm"); + const client = await makeWasmClient(wasmd.endpoint); + const beneficiaryAddress = makeRandomAddress(); + const transferAmount = coins(707707, "ucosm"); + + // create new instance and compare before and after + const { contractInfos: existingContractInfos } = await client.unverified.wasm.listContractsByCodeId( + hackatomCodeId, + ); + assert(existingContractInfos); + for (const { address, contractInfo } of existingContractInfos) { + expect(address).toMatch(bech32AddressMatcher); + expect(contractInfo!.codeId!.toNumber()).toEqual(hackatomCodeId); + expect(contractInfo!.creator).toMatch(bech32AddressMatcher); + expect(contractInfo!.label).toMatch(/^.+$/); + } + + const result = await instantiateContract(wallet, hackatomCodeId, beneficiaryAddress, transferAmount); + assertIsBroadcastTxSuccess(result); + const myAddress = JSON.parse(result.rawLog!)[0] + .events.find((event: any) => event.type === "message") + .attributes!.find((attribute: any) => attribute.key === "contract_address").value; + + const { contractInfos: newContractInfos } = await client.unverified.wasm.listContractsByCodeId( + hackatomCodeId, + ); + assert(newContractInfos); + expect(newContractInfos.length).toEqual(existingContractInfos.length + 1); + const newContract = newContractInfos[newContractInfos.length - 1]; + expect({ ...newContract.contractInfo }).toEqual({ + codeId: Long.fromNumber(hackatomCodeId, true), + creator: alice.address0, + label: "my escrow", + }); + + const { contractInfo } = await client.unverified.wasm.getContractInfo(myAddress); + assert(contractInfo); + expect({ ...contractInfo }).toEqual({ + codeId: Long.fromNumber(hackatomCodeId, true), + creator: alice.address0, + label: "my escrow", + }); + expect(contractInfo.admin).toEqual(""); + }); + + it("rejects for non-existent address", async () => { + pendingWithoutWasmd(); + assert(hackatomCodeId); + const client = await makeWasmClient(wasmd.endpoint); + const nonExistentAddress = makeRandomAddress(); + await expectAsync(client.unverified.wasm.getContractInfo(nonExistentAddress)).toBeRejectedWithError( + /not found/i, + ); + }); + }); + + describe("getContractCodeHistory", () => { + it("can list contract history", async () => { + pendingWithoutWasmd(); + assert(hackatomCodeId); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, "wasm"); + const client = await makeWasmClient(wasmd.endpoint); + const beneficiaryAddress = makeRandomAddress(); + const transferAmount = coins(707707, "ucosm"); + + // create new instance and compare before and after + const result = await instantiateContract(wallet, hackatomCodeId, beneficiaryAddress, transferAmount); + assertIsBroadcastTxSuccess(result); + + const myAddress = JSON.parse(result.rawLog!)[0] + .events.find((event: any) => event.type === "message") + .attributes!.find((attribute: any) => attribute.key === "contract_address").value; + + const history = await client.unverified.wasm.getContractCodeHistory(myAddress); + assert(history.entries); + expect(history.entries).toContain( + jasmine.objectContaining({ + codeId: Long.fromNumber(hackatomCodeId, true), + operation: + cosmwasm.wasm.v1beta1.ContractCodeHistoryOperationType.CONTRACT_CODE_HISTORY_OPERATION_TYPE_INIT, + msg: toAscii( + JSON.stringify({ + verifier: alice.address0, + beneficiary: beneficiaryAddress, + }), + ), + }), + ); + }); + + it("returns empty list for non-existent address", async () => { + pendingWithoutWasmd(); + assert(hackatomCodeId); + const client = await makeWasmClient(wasmd.endpoint); + const nonExistentAddress = makeRandomAddress(); + const history = await client.unverified.wasm.getContractCodeHistory(nonExistentAddress); + expect(history.entries).toEqual([]); + }); + }); + + describe("getAllContractState", () => { + it("can get all state", async () => { + pendingWithoutWasmd(); + assert(hackatomContractAddress); + const client = await makeWasmClient(wasmd.endpoint); + const { models } = await client.unverified.wasm.getAllContractState(hackatomContractAddress); + assert(models); + expect(models.length).toEqual(1); + const data = models[0]; + expect(data.key).toEqual(hackatomConfigKey); + const value = JSON.parse(fromAscii(data.value!)); + expect(value.verifier).toMatch(base64Matcher); + expect(value.beneficiary).toMatch(base64Matcher); + }); + + it("rejects for non-existent address", async () => { + pendingWithoutWasmd(); + const client = await makeWasmClient(wasmd.endpoint); + const nonExistentAddress = makeRandomAddress(); + await expectAsync(client.unverified.wasm.getAllContractState(nonExistentAddress)).toBeRejectedWithError( + /not found/i, + ); + }); + }); + + describe("queryContractRaw", () => { + it("can query by key", async () => { + pendingWithoutWasmd(); + assert(hackatomContractAddress); + const client = await makeWasmClient(wasmd.endpoint); + const raw = await client.unverified.wasm.queryContractRaw(hackatomContractAddress, hackatomConfigKey); + assert(raw.data, "must get result"); + const model = JSON.parse(fromAscii(raw.data)); + expect(model.verifier).toMatch(base64Matcher); + expect(model.beneficiary).toMatch(base64Matcher); + }); + + it("returns empty object for missing key", async () => { + pendingWithoutWasmd(); + assert(hackatomContractAddress); + const client = await makeWasmClient(wasmd.endpoint); + const response = await client.unverified.wasm.queryContractRaw( + hackatomContractAddress, + fromHex("cafe0dad"), + ); + expect({ ...response }).toEqual({}); + }); + + it("returns null for non-existent address", async () => { + pendingWithoutWasmd(); + const client = await makeWasmClient(wasmd.endpoint); + const nonExistentAddress = makeRandomAddress(); + await expectAsync( + client.unverified.wasm.queryContractRaw(nonExistentAddress, hackatomConfigKey), + ).toBeRejectedWithError(/not found/i); + }); + }); + + describe("queryContractSmart", () => { + it("can make smart queries", async () => { + pendingWithoutWasmd(); + assert(hackatomContractAddress); + const client = await makeWasmClient(wasmd.endpoint); + const request = { verifier: {} }; + const { data } = await client.unverified.wasm.queryContractSmart(hackatomContractAddress, request); + assert(data); + const parsedData = JSON.parse(fromAscii(data)); + expect(parsedData).toEqual({ verifier: alice.address0 }); + }); + + it("throws for invalid query requests", async () => { + pendingWithoutWasmd(); + assert(hackatomContractAddress); + const client = await makeWasmClient(wasmd.endpoint); + const request = { nosuchkey: {} }; + await expectAsync( + client.unverified.wasm.queryContractSmart(hackatomContractAddress, request), + ).toBeRejectedWithError(/Error parsing into type hackatom::contract::QueryMsg: unknown variant/i); + }); + + it("throws for non-existent address", async () => { + pendingWithoutWasmd(); + const client = await makeWasmClient(wasmd.endpoint); + const nonExistentAddress = makeRandomAddress(); + const request = { verifier: {} }; + await expectAsync( + client.unverified.wasm.queryContractSmart(nonExistentAddress, request), + ).toBeRejectedWithError(/not found/i); + }); + }); + + describe("broadcastTx", () => { + it("can upload, instantiate and execute wasm", async () => { + pendingWithoutWasmd(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); + const client = await makeWasmClient(wasmd.endpoint); + + const transferAmount = [coin(1234, "ucosm"), coin(321, "ustake")]; + const beneficiaryAddress = makeRandomAddress(); + + let codeId: number; + + // upload + { + const result = await uploadContract(wallet, getHackatom()); + assertIsBroadcastTxSuccess(result); + const parsedLogs = logs.parseLogs(parseRawLog(result.rawLog)); + const codeIdAttr = logs.findAttribute(parsedLogs, "message", "code_id"); + codeId = Number.parseInt(codeIdAttr.value, 10); + expect(codeId).toBeGreaterThanOrEqual(1); + expect(codeId).toBeLessThanOrEqual(200); + expect(result.data!.length).toEqual(1); + expect({ ...result.data![0] }).toEqual({ + msgType: "store-code", + data: toAscii(`${codeId}`), + }); + } + + let contractAddress: string; + + // instantiate + { + const result = await instantiateContract(wallet, codeId, beneficiaryAddress, transferAmount); + assertIsBroadcastTxSuccess(result); + const parsedLogs = logs.parseLogs(parseRawLog(result.rawLog)); + const contractAddressAttr = logs.findAttribute(parsedLogs, "message", "contract_address"); + contractAddress = contractAddressAttr.value; + const amountAttr = logs.findAttribute(parsedLogs, "transfer", "amount"); + expect(amountAttr.value).toEqual("1234ucosm,321ustake"); + expect(result.data!.length).toEqual(1); + expect({ ...result.data![0] }).toEqual({ + msgType: "instantiate", + data: toAscii(contractAddress), + }); + + const balanceUcosm = await client.bank.balance(contractAddress, "ucosm"); + expect(balanceUcosm).toEqual(transferAmount[0]); + const balanceUstake = await client.bank.balance(contractAddress, "ustake"); + expect(balanceUstake).toEqual(transferAmount[1]); + } + + // execute + { + const result = await executeContract(wallet, contractAddress, { release: {} }); + assertIsBroadcastTxSuccess(result); + expect(result.data!.length).toEqual(1); + expect({ ...result.data![0] }).toEqual({ + msgType: "execute", + data: fromHex("F00BAA"), + }); + const parsedLogs = logs.parseLogs(parseRawLog(result.rawLog)); + const wasmEvent = parsedLogs.find(() => true)?.events.find((e) => e.type === "wasm"); + assert(wasmEvent, "Event of type wasm expected"); + expect(wasmEvent.attributes).toContain({ key: "action", value: "release" }); + expect(wasmEvent.attributes).toContain({ + key: "destination", + value: beneficiaryAddress, + }); + + // Verify token transfer from contract to beneficiary + const beneficiaryBalanceUcosm = await client.bank.balance(beneficiaryAddress, "ucosm"); + expect(beneficiaryBalanceUcosm).toEqual(transferAmount[0]); + const beneficiaryBalanceUstake = await client.bank.balance(beneficiaryAddress, "ustake"); + expect(beneficiaryBalanceUstake).toEqual(transferAmount[1]); + } + }); + }); +});