Merge pull request #615 from cosmos/594-amino-signing-cosmwasm

Implement Amino signing for bank/staking/wasm in cosmwasm-stargate
This commit is contained in:
Simon Warta 2021-01-13 18:16:44 +01:00 committed by GitHub
commit 9651c5e3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1055 additions and 183 deletions

View File

@ -49,6 +49,14 @@
- @cosmjs/tendermint-rpc: Remove types `BlockHash`, `TxBytes` and `TxHash`. Use
`Uint8Array` instead.
### Added
- @cosmjs/utils: Added `assertDefinedAndNotNull`.
### Removed
- @cosmjs/utils: `assertDefined` removed in favour of `assertDefinedAndNotNull`.
## 0.23.2 (2021-01-06)
### Security

View File

@ -0,0 +1,337 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {
MsgClearAdmin,
MsgExecuteContract,
MsgInstantiateContract,
MsgMigrateContract,
MsgStoreCode,
MsgUpdateAdmin,
} from "@cosmjs/cosmwasm-launchpad";
import { fromBase64, toUtf8 } from "@cosmjs/encoding";
import { coins } from "@cosmjs/launchpad";
import { AminoTypes } from "@cosmjs/stargate";
import Long from "long";
import { cosmWasmTypes } from "./aminotypes";
import { cosmwasm } from "./codec";
type IMsgStoreCode = cosmwasm.wasm.v1beta1.IMsgStoreCode;
type IMsgInstantiateContract = cosmwasm.wasm.v1beta1.IMsgInstantiateContract;
type IMsgUpdateAdmin = cosmwasm.wasm.v1beta1.IMsgUpdateAdmin;
type IMsgClearAdmin = cosmwasm.wasm.v1beta1.IMsgClearAdmin;
type IMsgExecuteContract = cosmwasm.wasm.v1beta1.IMsgExecuteContract;
type IMsgMigrateContract = cosmwasm.wasm.v1beta1.IMsgMigrateContract;
describe("AminoTypes", () => {
describe("toAmino", () => {
it("works for MsgStoreCode", () => {
const msg: IMsgStoreCode = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
wasmByteCode: fromBase64("WUVMTE9XIFNVQk1BUklORQ=="),
source: "Arrabiata",
builder: "Bob",
};
const aminoMsg = new AminoTypes({ additions: cosmWasmTypes }).toAmino({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgStoreCode",
value: msg,
});
const expected: MsgStoreCode = {
type: "wasm/MsgStoreCode",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
wasm_byte_code: "WUVMTE9XIFNVQk1BUklORQ==",
source: "Arrabiata",
builder: "Bob",
},
};
expect(aminoMsg).toEqual(expected);
});
it("works for MsgInstantiateContract", () => {
const msg: IMsgInstantiateContract = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
codeId: Long.fromString("12345"),
label: "sticky",
initMsg: toUtf8(
JSON.stringify({
foo: "bar",
}),
),
initFunds: coins(1234, "ucosm"),
admin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
};
const aminoMsg = new AminoTypes({ additions: cosmWasmTypes }).toAmino({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgInstantiateContract",
value: msg,
});
const expected: MsgInstantiateContract = {
type: "wasm/MsgInstantiateContract",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
code_id: "12345",
label: "sticky",
init_msg: {
foo: "bar",
},
init_funds: coins(1234, "ucosm"),
admin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
},
};
expect(aminoMsg).toEqual(expected);
});
it("works for MsgUpdateAdmin", () => {
const msg: IMsgUpdateAdmin = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
newAdmin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
};
const aminoMsg = new AminoTypes({ additions: cosmWasmTypes }).toAmino({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgUpdateAdmin",
value: msg,
});
const expected: MsgUpdateAdmin = {
type: "wasm/MsgUpdateAdmin",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
new_admin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
},
};
expect(aminoMsg).toEqual(expected);
});
it("works for MsgClearAdmin", () => {
const msg: IMsgClearAdmin = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
};
const aminoMsg = new AminoTypes({ additions: cosmWasmTypes }).toAmino({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgClearAdmin",
value: msg,
});
const expected: MsgClearAdmin = {
type: "wasm/MsgClearAdmin",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
},
};
expect(aminoMsg).toEqual(expected);
});
it("works for MsgExecuteContract", () => {
const msg: IMsgExecuteContract = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
msg: toUtf8(
JSON.stringify({
foo: "bar",
}),
),
sentFunds: coins(1234, "ucosm"),
};
const aminoMsg = new AminoTypes({ additions: cosmWasmTypes }).toAmino({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgExecuteContract",
value: msg,
});
const expected: MsgExecuteContract = {
type: "wasm/MsgExecuteContract",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
msg: {
foo: "bar",
},
sent_funds: coins(1234, "ucosm"),
},
};
expect(aminoMsg).toEqual(expected);
});
it("works for MsgMigrateContract", () => {
const msg: IMsgMigrateContract = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
codeId: Long.fromString("98765"),
migrateMsg: toUtf8(
JSON.stringify({
foo: "bar",
}),
),
};
const aminoMsg = new AminoTypes({ additions: cosmWasmTypes }).toAmino({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgMigrateContract",
value: msg,
});
const expected: MsgMigrateContract = {
type: "wasm/MsgMigrateContract",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
code_id: "98765",
msg: {
foo: "bar",
},
},
};
expect(aminoMsg).toEqual(expected);
});
});
describe("fromAmino", () => {
it("works for MsgStoreCode", () => {
const aminoMsg: MsgStoreCode = {
type: "wasm/MsgStoreCode",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
wasm_byte_code: "WUVMTE9XIFNVQk1BUklORQ==",
source: "Arrabiata",
builder: "Bob",
},
};
const msg = new AminoTypes({ additions: cosmWasmTypes }).fromAmino(aminoMsg);
const expectedValue: IMsgStoreCode = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
wasmByteCode: fromBase64("WUVMTE9XIFNVQk1BUklORQ=="),
source: "Arrabiata",
builder: "Bob",
};
expect(msg).toEqual({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgStoreCode",
value: expectedValue,
});
});
it("works for MsgInstantiateContract", () => {
const aminoMsg: MsgInstantiateContract = {
type: "wasm/MsgInstantiateContract",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
code_id: "12345",
label: "sticky",
init_msg: {
foo: "bar",
},
init_funds: coins(1234, "ucosm"),
admin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
},
};
const msg = new AminoTypes({ additions: cosmWasmTypes }).fromAmino(aminoMsg);
const expectedValue: IMsgInstantiateContract = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
codeId: Long.fromString("12345"),
label: "sticky",
initMsg: toUtf8(
JSON.stringify({
foo: "bar",
}),
),
initFunds: coins(1234, "ucosm"),
admin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
};
expect(msg).toEqual({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgInstantiateContract",
value: expectedValue,
});
});
it("works for MsgUpdateAdmin", () => {
const aminoMsg: MsgUpdateAdmin = {
type: "wasm/MsgUpdateAdmin",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
new_admin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
},
};
const msg = new AminoTypes({ additions: cosmWasmTypes }).fromAmino(aminoMsg);
const expectedValue: IMsgUpdateAdmin = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
newAdmin: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
};
expect(msg).toEqual({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgUpdateAdmin",
value: expectedValue,
});
});
it("works for MsgClearAdmin", () => {
const aminoMsg: MsgClearAdmin = {
type: "wasm/MsgClearAdmin",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
},
};
const msg = new AminoTypes({ additions: cosmWasmTypes }).fromAmino(aminoMsg);
const expectedValue: IMsgClearAdmin = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
};
expect(msg).toEqual({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgClearAdmin",
value: expectedValue,
});
});
it("works for MsgExecuteContract", () => {
const aminoMsg: MsgExecuteContract = {
type: "wasm/MsgExecuteContract",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
msg: {
foo: "bar",
},
sent_funds: coins(1234, "ucosm"),
},
};
const msg = new AminoTypes({ additions: cosmWasmTypes }).fromAmino(aminoMsg);
const expectedValue: IMsgExecuteContract = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
msg: toUtf8(
JSON.stringify({
foo: "bar",
}),
),
sentFunds: coins(1234, "ucosm"),
};
expect(msg).toEqual({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgExecuteContract",
value: expectedValue,
});
});
it("works for MsgMigrateContract", () => {
const aminoMsg: MsgMigrateContract = {
type: "wasm/MsgMigrateContract",
value: {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
code_id: "98765",
msg: {
foo: "bar",
},
},
};
const msg = new AminoTypes({ additions: cosmWasmTypes }).fromAmino(aminoMsg);
const expectedValue: IMsgMigrateContract = {
sender: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
contract: "cosmos1xy4yqngt0nlkdcenxymg8tenrghmek4nmqm28k",
codeId: Long.fromString("98765"),
migrateMsg: toUtf8(
JSON.stringify({
foo: "bar",
}),
),
};
expect(msg).toEqual({
typeUrl: "/cosmwasm.wasm.v1beta1.MsgMigrateContract",
value: expectedValue,
});
});
});
});

View File

@ -0,0 +1,161 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {
MsgClearAdmin,
MsgExecuteContract,
MsgInstantiateContract,
MsgMigrateContract,
MsgStoreCode,
MsgUpdateAdmin,
} from "@cosmjs/cosmwasm-launchpad";
import { fromBase64, fromUtf8, toBase64, toUtf8 } from "@cosmjs/encoding";
import { AminoConverter, coinFromProto } from "@cosmjs/stargate";
import { assertDefinedAndNotNull } from "@cosmjs/utils";
import Long from "long";
import { cosmwasm } from "./codec";
type IMsgStoreCode = cosmwasm.wasm.v1beta1.IMsgStoreCode;
type IMsgInstantiateContract = cosmwasm.wasm.v1beta1.IMsgInstantiateContract;
type IMsgUpdateAdmin = cosmwasm.wasm.v1beta1.IMsgUpdateAdmin;
type IMsgClearAdmin = cosmwasm.wasm.v1beta1.IMsgClearAdmin;
type IMsgExecuteContract = cosmwasm.wasm.v1beta1.IMsgExecuteContract;
type IMsgMigrateContract = cosmwasm.wasm.v1beta1.IMsgMigrateContract;
export const cosmWasmTypes: Record<string, AminoConverter> = {
"/cosmwasm.wasm.v1beta1.MsgStoreCode": {
aminoType: "wasm/MsgStoreCode",
toAmino: ({ sender, wasmByteCode, source, builder }: IMsgStoreCode): MsgStoreCode["value"] => {
assertDefinedAndNotNull(sender, "missing sender");
assertDefinedAndNotNull(wasmByteCode, "missing wasmByteCode");
assertDefinedAndNotNull(source, "missing source");
assertDefinedAndNotNull(builder, "missing builder");
return {
sender: sender,
wasm_byte_code: toBase64(wasmByteCode),
source: source,
builder: builder,
};
},
fromAmino: ({ sender, wasm_byte_code, source, builder }: MsgStoreCode["value"]): IMsgStoreCode => ({
sender: sender,
wasmByteCode: fromBase64(wasm_byte_code),
source: source,
builder: builder,
}),
},
"/cosmwasm.wasm.v1beta1.MsgInstantiateContract": {
aminoType: "wasm/MsgInstantiateContract",
toAmino: ({
sender,
codeId,
label,
initMsg,
initFunds,
admin,
}: IMsgInstantiateContract): MsgInstantiateContract["value"] => {
assertDefinedAndNotNull(sender, "missing sender");
assertDefinedAndNotNull(codeId, "missing codeId");
assertDefinedAndNotNull(label, "missing label");
assertDefinedAndNotNull(initMsg, "missing initMsg");
assertDefinedAndNotNull(initFunds, "missing initFunds");
return {
sender: sender,
code_id: codeId.toString(),
label: label,
init_msg: JSON.parse(fromUtf8(initMsg)),
init_funds: initFunds.map(coinFromProto),
admin: admin ?? undefined,
};
},
fromAmino: ({
sender,
code_id,
label,
init_msg,
init_funds,
admin,
}: MsgInstantiateContract["value"]): IMsgInstantiateContract => ({
sender: sender,
codeId: Long.fromString(code_id),
label: label,
initMsg: toUtf8(JSON.stringify(init_msg)),
initFunds: [...init_funds],
admin: admin,
}),
},
"/cosmwasm.wasm.v1beta1.MsgUpdateAdmin": {
aminoType: "wasm/MsgUpdateAdmin",
toAmino: ({ sender, newAdmin, contract }: IMsgUpdateAdmin): MsgUpdateAdmin["value"] => {
assertDefinedAndNotNull(sender, "missing sender");
assertDefinedAndNotNull(newAdmin, "missing newAdmin");
assertDefinedAndNotNull(contract, "missing contract");
return {
sender: sender,
new_admin: newAdmin,
contract: contract,
};
},
fromAmino: ({ sender, new_admin, contract }: MsgUpdateAdmin["value"]): IMsgUpdateAdmin => ({
sender: sender,
newAdmin: new_admin,
contract: contract,
}),
},
"/cosmwasm.wasm.v1beta1.MsgClearAdmin": {
aminoType: "wasm/MsgClearAdmin",
toAmino: ({ sender, contract }: IMsgClearAdmin): MsgClearAdmin["value"] => {
assertDefinedAndNotNull(sender, "missing sender");
assertDefinedAndNotNull(contract, "missing contract");
return {
sender: sender,
contract: contract,
};
},
fromAmino: ({ sender, contract }: MsgClearAdmin["value"]): IMsgClearAdmin => ({
sender: sender,
contract: contract,
}),
},
"/cosmwasm.wasm.v1beta1.MsgExecuteContract": {
aminoType: "wasm/MsgExecuteContract",
toAmino: ({ sender, contract, msg, sentFunds }: IMsgExecuteContract): MsgExecuteContract["value"] => {
assertDefinedAndNotNull(sender, "missing sender");
assertDefinedAndNotNull(contract, "missing contract");
assertDefinedAndNotNull(msg, "missing msg");
assertDefinedAndNotNull(sentFunds, "missing sentFunds");
return {
sender: sender,
contract: contract,
msg: JSON.parse(fromUtf8(msg)),
sent_funds: sentFunds.map(coinFromProto),
};
},
fromAmino: ({ sender, contract, msg, sent_funds }: MsgExecuteContract["value"]): IMsgExecuteContract => ({
sender: sender,
contract: contract,
msg: toUtf8(JSON.stringify(msg)),
sentFunds: [...sent_funds],
}),
},
"/cosmwasm.wasm.v1beta1.MsgMigrateContract": {
aminoType: "wasm/MsgMigrateContract",
toAmino: ({ sender, contract, codeId, migrateMsg }: IMsgMigrateContract): MsgMigrateContract["value"] => {
assertDefinedAndNotNull(sender, "missing sender");
assertDefinedAndNotNull(contract, "missing contract");
assertDefinedAndNotNull(codeId, "missing codeId");
assertDefinedAndNotNull(migrateMsg, "missing migrateMsg");
return {
sender: sender,
contract: contract,
code_id: codeId.toString(),
msg: JSON.parse(fromUtf8(migrateMsg)),
};
},
fromAmino: ({ sender, contract, code_id, msg }: MsgMigrateContract["value"]): IMsgMigrateContract => ({
sender: sender,
contract: contract,
codeId: Long.fromString(code_id),
migrateMsg: toUtf8(JSON.stringify(msg)),
}),
},
};

View File

@ -9,7 +9,7 @@ import {
parseRawLog,
SigningStargateClient,
} from "@cosmjs/stargate";
import { assert, assertDefined } from "@cosmjs/utils";
import { assert, assertDefinedAndNotNull } from "@cosmjs/utils";
import Long from "long";
import { cosmwasm } from "../codec";
@ -395,7 +395,7 @@ describe("WasmExtension", () => {
expect(codeId).toBeGreaterThanOrEqual(1);
expect(codeId).toBeLessThanOrEqual(200);
assertDefined(result.data);
assertDefinedAndNotNull(result.data);
const msgData = fromOneElementArray(result.data);
expect(msgData.msgType).toEqual("store-code");
expect(MsgStoreCodeResponse.decode(msgData.data!)).toEqual(
@ -415,7 +415,7 @@ describe("WasmExtension", () => {
const amountAttr = logs.findAttribute(parsedLogs, "transfer", "amount");
expect(amountAttr.value).toEqual("1234ucosm,321ustake");
assertDefined(result.data);
assertDefinedAndNotNull(result.data);
const msgData = fromOneElementArray(result.data);
expect(msgData.msgType).toEqual("instantiate");
expect(MsgInstantiateContractResponse.decode(msgData.data!)).toEqual(
@ -441,7 +441,7 @@ describe("WasmExtension", () => {
value: beneficiaryAddress,
});
assertDefined(result.data);
assertDefinedAndNotNull(result.data);
const msgData = fromOneElementArray(result.data);
expect(msgData.msgType).toEqual("execute");
expect(MsgExecuteContractResponse.decode(msgData.data!)).toEqual(

View File

@ -2,12 +2,21 @@
import { UploadMeta } from "@cosmjs/cosmwasm-launchpad";
import { sha256 } from "@cosmjs/crypto";
import { toHex } from "@cosmjs/encoding";
import { coin, coins, GasPrice } from "@cosmjs/launchpad";
import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing";
import { assertIsBroadcastTxSuccess, codec } from "@cosmjs/stargate";
import {
coin,
coins,
GasPrice,
MsgDelegate as LaunchpadMsgDelegate,
Secp256k1HdWallet,
} from "@cosmjs/launchpad";
import { Coin, cosmosField, DirectSecp256k1HdWallet, registered, Registry } from "@cosmjs/proto-signing";
import { AminoTypes, assertIsBroadcastTxSuccess, codec } from "@cosmjs/stargate";
import { assert, sleep } from "@cosmjs/utils";
import Long from "long";
import pako from "pako";
import { Message } from "protobufjs";
import { cosmwasm } from "./codec";
import { PrivateSigningCosmWasmClient, SigningCosmWasmClient } from "./signingcosmwasmclient";
import {
alice,
@ -15,12 +24,18 @@ import {
makeRandomAddress,
makeWasmClient,
ModifyingDirectSecp256k1HdWallet,
ModifyingSecp256k1HdWallet,
pendingWithoutWasmd,
unused,
validator,
wasmd,
} from "./testutils.spec";
type IMsgSend = codec.cosmos.bank.v1beta1.IMsgSend;
type IMsgDelegate = codec.cosmos.staking.v1beta1.IMsgDelegate;
type IMsgStoreCode = cosmwasm.wasm.v1beta1.IMsgStoreCode;
const { MsgSend } = codec.cosmos.bank.v1beta1;
const { MsgDelegate } = codec.cosmos.staking.v1beta1;
const { Tx } = codec.cosmos.tx.v1beta1;
@ -29,15 +44,30 @@ describe("SigningCosmWasmClient", () => {
it("can be constructed", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
expect(client).toBeTruthy();
});
it("can be constructed with custom registry", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic);
const registry = new Registry();
registry.register("/custom.MsgCustom", MsgSend);
const options = { prefix: wasmd.prefix, registry: registry };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.registry.lookupType("/custom.MsgCustom")).toEqual(MsgSend);
});
it("can be constructed with custom gas price", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const gasPrice = GasPrice.fromString("3.14utest");
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, { gasPrice });
const options = {
prefix: wasmd.prefix,
gasPrice: GasPrice.fromString("3.14utest"),
};
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.fees).toEqual({
upload: {
@ -70,10 +100,13 @@ describe("SigningCosmWasmClient", () => {
it("can be constructed with custom gas limits", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const gasLimits = {
send: 160000,
const options = {
prefix: wasmd.prefix,
gasLimits: {
send: 160000,
},
};
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, { gasLimits });
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.fees).toEqual({
upload: {
@ -106,14 +139,14 @@ describe("SigningCosmWasmClient", () => {
it("can be constructed with custom gas price and gas limits", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const gasPrice = GasPrice.fromString("3.14utest");
const gasLimits = {
send: 160000,
const options = {
prefix: wasmd.prefix,
gasPrice: GasPrice.fromString("3.14utest"),
gasLimits: {
send: 160000,
},
};
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, {
gasPrice,
gasLimits,
});
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.fees).toEqual({
upload: {
@ -148,7 +181,8 @@ describe("SigningCosmWasmClient", () => {
it("works", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const wasm = getHackatom().data;
const {
codeId,
@ -167,7 +201,8 @@ describe("SigningCosmWasmClient", () => {
it("can set builder and source", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const hackatom = getHackatom();
const meta: UploadMeta = {
source: "https://crates.io/api/v1/crates/cw-nameservice/0.1.0/download",
@ -184,7 +219,8 @@ describe("SigningCosmWasmClient", () => {
it("works with transfer amount", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId } = await client.upload(alice.address0, getHackatom().data);
const transferAmount = [coin(1234, "ucosm"), coin(321, "ustake")];
const beneficiaryAddress = makeRandomAddress();
@ -211,7 +247,8 @@ describe("SigningCosmWasmClient", () => {
it("works with admin", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress();
const { contractAddress } = await client.instantiate(
@ -233,7 +270,8 @@ describe("SigningCosmWasmClient", () => {
it("can instantiate one code multiple times", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId } = await client.upload(alice.address0, getHackatom().data);
const contractAddress1 = await client.instantiate(
alice.address0,
@ -261,7 +299,8 @@ describe("SigningCosmWasmClient", () => {
it("can update an admin", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress();
const { contractAddress } = await client.instantiate(
@ -296,7 +335,8 @@ describe("SigningCosmWasmClient", () => {
it("can clear an admin", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress();
const { contractAddress } = await client.instantiate(
@ -331,7 +371,8 @@ describe("SigningCosmWasmClient", () => {
it("can can migrate from one code ID to another", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId: codeId1 } = await client.upload(alice.address0, getHackatom().data);
const { codeId: codeId2 } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress();
@ -371,7 +412,8 @@ describe("SigningCosmWasmClient", () => {
it("works", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { codeId } = await client.upload(alice.address0, getHackatom().data);
// instantiate
const transferAmount = [coin(233444, "ucosm"), coin(5454, "ustake")];
@ -411,10 +453,36 @@ describe("SigningCosmWasmClient", () => {
});
describe("sendTokens", () => {
it("works", async () => {
it("works with direct signer", async () => {
pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const transferAmount = coins(7890, "ucosm");
const beneficiaryAddress = makeRandomAddress();
const memo = "for dinner";
// no tokens here
const before = await client.getBalance(beneficiaryAddress, "ucosm");
expect(before).toBeNull();
// send
const result = await client.sendTokens(alice.address0, beneficiaryAddress, transferAmount, memo);
assertIsBroadcastTxSuccess(result);
expect(result.rawLog).toBeTruthy();
// got tokens
const after = await client.getBalance(beneficiaryAddress, "ucosm");
assert(after);
expect(after).toEqual(transferAmount[0]);
});
it("works with legacy Amino signer", async () => {
pendingWithoutWasmd();
const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const transferAmount = coins(7890, "ucosm");
const beneficiaryAddress = makeRandomAddress();
@ -444,7 +512,7 @@ describe("SigningCosmWasmClient", () => {
const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate";
const registry = new Registry();
registry.register(msgDelegateTypeUrl, MsgDelegate);
const options = { registry: registry };
const options = { prefix: wasmd.prefix, registry: registry };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msg = MsgDelegate.create({
@ -475,7 +543,7 @@ describe("SigningCosmWasmClient", () => {
const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate";
const registry = new Registry();
registry.register(msgDelegateTypeUrl, MsgDelegate);
const options = { registry: registry };
const options = { prefix: wasmd.prefix, registry: registry };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msg = MsgDelegate.create({
@ -506,5 +574,193 @@ describe("SigningCosmWasmClient", () => {
expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333);
});
});
describe("legacy Amino mode", () => {
// NOTE: One custom registry shared between tests
// See https://github.com/protobufjs/protobuf.js#using-decorators
// > Decorated types reside in protobuf.roots["decorated"] using a flat structure, so no duplicate names.
const customRegistry = new Registry();
const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate";
@registered(customRegistry, msgDelegateTypeUrl)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
class CustomMsgDelegate extends Message {
@cosmosField.string(1)
public readonly custom_delegator_address?: string;
@cosmosField.string(2)
public readonly custom_validator_address?: string;
@cosmosField.message(3, Coin)
public readonly custom_amount?: Coin;
}
it("works with bank MsgSend", async () => {
pendingWithoutWasmd();
const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msgSend: IMsgSend = {
fromAddress: alice.address0,
toAddress: makeRandomAddress(),
amount: coins(1234, "ucosm"),
};
const msgAny = {
typeUrl: "/cosmos.bank.v1beta1.MsgSend",
value: msgSend,
};
const fee = {
amount: coins(2000, "ucosm"),
gas: "200000",
};
const memo = "Use your tokens wisely";
const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo);
assertIsBroadcastTxSuccess(result);
});
it("works with staking MsgDelegate", async () => {
pendingWithoutWasmd();
const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msgDelegate: IMsgDelegate = {
delegatorAddress: alice.address0,
validatorAddress: validator.validatorAddress,
amount: coin(1234, "ustake"),
};
const msgAny = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: msgDelegate,
};
const fee = {
amount: coins(2000, "ustake"),
gas: "200000",
};
const memo = "Use your tokens wisely";
const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo);
assertIsBroadcastTxSuccess(result);
});
it("works with wasm MsgStoreCode", async () => {
pendingWithoutWasmd();
const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const { data, builder, source } = getHackatom();
const msgStoreCode: IMsgStoreCode = {
sender: alice.address0,
wasmByteCode: pako.gzip(data),
source: source,
builder: builder,
};
const msgAny = {
typeUrl: "/cosmwasm.wasm.v1beta1.MsgStoreCode",
value: msgStoreCode,
};
const fee = {
amount: coins(2000, "ustake"),
gas: "1500000",
};
const memo = "Use your tokens wisely";
const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo);
assertIsBroadcastTxSuccess(result);
});
it("works with a custom registry and custom message", async () => {
pendingWithoutWasmd();
const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const customAminoTypes = new AminoTypes({
prefix: wasmd.prefix,
additions: {
"/cosmos.staking.v1beta1.MsgDelegate": {
aminoType: "cosmos-sdk/MsgDelegate",
toAmino: ({
custom_delegator_address,
custom_validator_address,
custom_amount,
}: CustomMsgDelegate): LaunchpadMsgDelegate["value"] => {
assert(custom_delegator_address, "missing custom_delegator_address");
assert(custom_validator_address, "missing validator_address");
assert(custom_amount, "missing amount");
assert(custom_amount.amount, "missing amount.amount");
assert(custom_amount.denom, "missing amount.denom");
return {
delegator_address: custom_delegator_address,
validator_address: custom_validator_address,
amount: {
amount: custom_amount.amount,
denom: custom_amount.denom,
},
};
},
fromAmino: ({
delegator_address,
validator_address,
amount,
}: LaunchpadMsgDelegate["value"]): CustomMsgDelegate =>
CustomMsgDelegate.create({
custom_delegator_address: delegator_address,
custom_validator_address: validator_address,
custom_amount: Coin.create(amount),
}),
},
},
});
const options = { prefix: wasmd.prefix, registry: customRegistry, aminoTypes: customAminoTypes };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msg = {
custom_delegator_address: alice.address0,
custom_validator_address: validator.validatorAddress,
custom_amount: coin(1234, "ustake"),
};
const msgAny = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: msg,
};
const fee = {
amount: coins(2000, "ucosm"),
gas: "200000",
};
const memo = "Use your power wisely";
const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo);
assertIsBroadcastTxSuccess(result);
});
it("works with a modifying signer", async () => {
pendingWithoutWasmd();
const wallet = await ModifyingSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, "wasm");
const options = { prefix: wasmd.prefix };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msg = {
delegatorAddress: alice.address0,
validatorAddress: validator.validatorAddress,
amount: coin(1234, "ustake"),
};
const msgAny = {
typeUrl: msgDelegateTypeUrl,
value: msg,
};
const fee = {
amount: coins(2000, "ucosm"),
gas: "200000",
};
const memo = "Use your power wisely";
const result = await client.signAndBroadcast(alice.address0, [msgAny], fee, memo);
assertIsBroadcastTxSuccess(result);
await sleep(1000);
const searchResult = await client.getTx(result.transactionHash);
assert(searchResult, "Must find transaction");
const tx = Tx.decode(searchResult.tx);
// From ModifyingSecp256k1HdWallet
expect(tx.body!.memo).toEqual("This was modified");
expect({ ...tx.authInfo!.fee!.amount![0] }).toEqual(coin(3000, "ucosm"));
expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333);
});
});
});
});

View File

@ -21,18 +21,21 @@ import {
GasLimits,
GasPrice,
logs,
makeSignDoc as makeSignDocAmino,
StdFee,
} from "@cosmjs/launchpad";
import { Int53, Uint53 } from "@cosmjs/math";
import {
EncodeObject,
encodePubkey,
isOfflineDirectSigner,
makeAuthInfoBytes,
makeSignDoc,
OfflineDirectSigner,
OfflineSigner,
Registry,
} from "@cosmjs/proto-signing";
import {
AminoTypes,
BroadcastTxFailure,
BroadcastTxResponse,
codec,
@ -43,10 +46,20 @@ import { adaptor34, Client as TendermintClient } from "@cosmjs/tendermint-rpc";
import Long from "long";
import pako from "pako";
import { cosmWasmTypes } from "./aminotypes";
import { cosmwasm } from "./codec";
import { CosmWasmClient } from "./cosmwasmclient";
const { SignMode } = codec.cosmos.tx.signing.v1beta1;
const { TxRaw } = codec.cosmos.tx.v1beta1;
const { MsgMultiSend } = codec.cosmos.bank.v1beta1;
const {
MsgBeginRedelegate,
MsgCreateValidator,
MsgDelegate,
MsgEditValidator,
MsgUndelegate,
} = codec.cosmos.staking.v1beta1;
const {
MsgClearAdmin,
MsgExecuteContract,
@ -81,6 +94,12 @@ function createBroadcastTxErrorMessage(result: BroadcastTxFailure): string {
function createDefaultRegistry(): Registry {
return new Registry([
["/cosmos.bank.v1beta1.MsgMultiSend", MsgMultiSend],
["/cosmos.staking.v1beta1.MsgBeginRedelegate", MsgBeginRedelegate],
["/cosmos.staking.v1beta1.MsgCreateValidator", MsgCreateValidator],
["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate],
["/cosmos.staking.v1beta1.MsgEditValidator", MsgEditValidator],
["/cosmos.staking.v1beta1.MsgUndelegate", MsgUndelegate],
["/cosmwasm.wasm.v1beta1.MsgClearAdmin", MsgClearAdmin],
["/cosmwasm.wasm.v1beta1.MsgExecuteContract", MsgExecuteContract],
["/cosmwasm.wasm.v1beta1.MsgMigrateContract", MsgMigrateContract],
@ -92,6 +111,8 @@ function createDefaultRegistry(): Registry {
export interface SigningCosmWasmClientOptions {
readonly registry?: Registry;
readonly aminoTypes?: AminoTypes;
readonly prefix?: string;
readonly gasPrice?: GasPrice;
readonly gasLimits?: GasLimits<CosmosFeeTable>;
}
@ -99,16 +120,18 @@ export interface SigningCosmWasmClientOptions {
/** Use for testing only */
export interface PrivateSigningCosmWasmClient {
readonly fees: CosmWasmFeeTable;
readonly registry: Registry;
}
export class SigningCosmWasmClient extends CosmWasmClient {
private readonly fees: CosmosFeeTable;
private readonly registry: Registry;
private readonly signer: OfflineDirectSigner;
private readonly signer: OfflineSigner;
private readonly aminoTypes: AminoTypes;
public static async connectWithSigner(
endpoint: string,
signer: OfflineDirectSigner,
signer: OfflineSigner,
options: SigningCosmWasmClientOptions = {},
): Promise<SigningCosmWasmClient> {
const tmClient = await TendermintClient.connect(endpoint, adaptor34);
@ -117,17 +140,19 @@ export class SigningCosmWasmClient extends CosmWasmClient {
private constructor(
tmClient: TendermintClient,
signer: OfflineDirectSigner,
signer: OfflineSigner,
options: SigningCosmWasmClientOptions,
) {
super(tmClient);
const {
registry = createDefaultRegistry(),
aminoTypes = new AminoTypes({ additions: cosmWasmTypes, prefix: options.prefix }),
gasPrice = defaultGasPrice,
gasLimits = defaultGasLimits,
} = options;
this.fees = buildFeeTable<CosmosFeeTable>(gasPrice, defaultGasLimits, gasLimits);
this.registry = registry;
this.aminoTypes = aminoTypes;
this.signer = signer;
}
@ -355,12 +380,44 @@ export class SigningCosmWasmClient extends CosmWasmClient {
});
const gasLimit = Int53.fromString(fee.gas).toNumber();
const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence);
const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber);
const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc);
if (isOfflineDirectSigner(this.signer)) {
const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence);
const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber);
const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc);
const txRaw = TxRaw.create({
bodyBytes: signed.bodyBytes,
authInfoBytes: signed.authInfoBytes,
signatures: [fromBase64(signature.signature)],
});
const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish());
return this.broadcastTx(signedTx);
}
// Amino signer
const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg));
const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence);
const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc);
const signedTxBody = {
messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)),
memo: signed.memo,
};
const signedTxBodyBytes = this.registry.encode({
typeUrl: "/cosmos.tx.v1beta1.TxBody",
value: signedTxBody,
});
const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber();
const signedSequence = Int53.fromString(signed.sequence).toNumber();
const signedAuthInfoBytes = makeAuthInfoBytes(
[pubkeyAny],
signed.fee.amount,
signedGasLimit,
signedSequence,
signMode,
);
const txRaw = TxRaw.create({
bodyBytes: signed.bodyBytes,
authInfoBytes: signed.authInfoBytes,
bodyBytes: signedTxBodyBytes,
authInfoBytes: signedAuthInfoBytes,
signatures: [fromBase64(signature.signature)],
});
const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish());

View File

@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto";
import { Bech32, fromBase64 } from "@cosmjs/encoding";
import { coins, makeCosmoshubPath } from "@cosmjs/launchpad";
import {
AminoSignResponse,
coins,
makeCosmoshubPath,
Secp256k1HdWallet,
StdSignDoc,
} from "@cosmjs/launchpad";
import { DirectSecp256k1HdWallet, DirectSignResponse, makeAuthInfoBytes } from "@cosmjs/proto-signing";
import {
AuthExtension,
@ -205,6 +211,41 @@ export async function makeWasmClient(
return QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension, setupWasmExtension);
}
/**
* A class for testing clients using an Amino signer which modifies the transaction it receives before signing
*/
export class ModifyingSecp256k1HdWallet extends Secp256k1HdWallet {
public static async fromMnemonic(
mnemonic: string,
hdPath = makeCosmoshubPath(0),
prefix = "cosmos",
): Promise<ModifyingSecp256k1HdWallet> {
const mnemonicChecked = new EnglishMnemonic(mnemonic);
const seed = await Bip39.mnemonicToSeed(mnemonicChecked);
const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, hdPath);
const uncompressed = (await Secp256k1.makeKeypair(privkey)).pubkey;
return new ModifyingSecp256k1HdWallet(
mnemonicChecked,
hdPath,
privkey,
Secp256k1.compressPubkey(uncompressed),
prefix,
);
}
public async signAmino(signerAddress: string, signDoc: StdSignDoc): Promise<AminoSignResponse> {
const modifiedSignDoc = {
...signDoc,
fee: {
amount: coins(3000, "ucosm"),
gas: "333333",
},
memo: "This was modified",
};
return super.signAmino(signerAddress, modifiedSignDoc);
}
}
/**
* A class for testing clients using a direct signer which modifies the transaction it receives before signing
*/

View File

@ -0,0 +1,2 @@
import { AminoConverter } from "@cosmjs/stargate";
export declare const cosmWasmTypes: Record<string, AminoConverter>;

View File

@ -9,25 +9,29 @@ import {
UploadResult,
} from "@cosmjs/cosmwasm-launchpad";
import { Coin, CosmosFeeTable, GasLimits, GasPrice, StdFee } from "@cosmjs/launchpad";
import { EncodeObject, OfflineDirectSigner, Registry } from "@cosmjs/proto-signing";
import { BroadcastTxResponse } from "@cosmjs/stargate";
import { EncodeObject, OfflineSigner, Registry } from "@cosmjs/proto-signing";
import { AminoTypes, BroadcastTxResponse } from "@cosmjs/stargate";
import { CosmWasmClient } from "./cosmwasmclient";
export interface SigningCosmWasmClientOptions {
readonly registry?: Registry;
readonly aminoTypes?: AminoTypes;
readonly prefix?: string;
readonly gasPrice?: GasPrice;
readonly gasLimits?: GasLimits<CosmosFeeTable>;
}
/** Use for testing only */
export interface PrivateSigningCosmWasmClient {
readonly fees: CosmWasmFeeTable;
readonly registry: Registry;
}
export declare class SigningCosmWasmClient extends CosmWasmClient {
private readonly fees;
private readonly registry;
private readonly signer;
private readonly aminoTypes;
static connectWithSigner(
endpoint: string,
signer: OfflineDirectSigner,
signer: OfflineSigner,
options?: SigningCosmWasmClientOptions,
): Promise<SigningCosmWasmClient>;
private constructor();

View File

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { fromBase64, toBase64 } from "@cosmjs/encoding";
import {
Coin,
decodeBech32Pubkey,
encodeBech32Pubkey,
Msg,
@ -14,11 +13,11 @@ import {
MsgUndelegate,
} from "@cosmjs/launchpad";
import { EncodeObject } from "@cosmjs/proto-signing";
import { assert } from "@cosmjs/utils";
import { assertDefinedAndNotNull } from "@cosmjs/utils";
import { cosmos } from "./codec";
import { coinFromProto } from "./stargateclient";
type ICoin = cosmos.base.v1beta1.ICoin;
type IMsgSend = cosmos.bank.v1beta1.IMsgSend;
type IMsgMultiSend = cosmos.bank.v1beta1.IMsgMultiSend;
type IMsgBeginRedelegate = cosmos.staking.v1beta1.IMsgBeginRedelegate;
@ -33,29 +32,18 @@ export interface AminoConverter {
readonly fromAmino: (value: any) => any;
}
function checkAmount(amount: readonly ICoin[] | undefined | null): readonly Coin[] {
assert(amount, "missing amount");
return amount.map((a) => {
assert(a.amount, "missing amount");
assert(a.denom, "missing denom");
return {
amount: a.amount,
denom: a.denom,
};
});
}
function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
return {
"/cosmos.bank.v1beta1.MsgSend": {
aminoType: "cosmos-sdk/MsgSend",
toAmino: ({ fromAddress, toAddress, amount }: IMsgSend): MsgSend["value"] => {
assert(fromAddress, "missing fromAddress");
assert(toAddress, "missing toAddress");
assertDefinedAndNotNull(fromAddress, "missing fromAddress");
assertDefinedAndNotNull(toAddress, "missing toAddress");
assertDefinedAndNotNull(amount, "missing amount");
return {
from_address: fromAddress,
to_address: toAddress,
amount: checkAmount(amount),
amount: amount.map(coinFromProto),
};
},
fromAmino: ({ from_address, to_address, amount }: MsgSend["value"]): IMsgSend => ({
@ -67,21 +55,23 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
"/cosmos.bank.v1beta1.MsgMultiSend": {
aminoType: "cosmos-sdk/MsgMultiSend",
toAmino: ({ inputs, outputs }: IMsgMultiSend): MsgMultiSend["value"] => {
assert(inputs, "missing inputs");
assert(outputs, "missing outputs");
assertDefinedAndNotNull(inputs, "missing inputs");
assertDefinedAndNotNull(outputs, "missing outputs");
return {
inputs: inputs.map((input) => {
assert(input.address, "missing input.address");
assertDefinedAndNotNull(input.address, "missing input.address");
assertDefinedAndNotNull(input.coins, "missing input.amount");
return {
address: input.address,
coins: checkAmount(input.coins),
coins: input.coins.map(coinFromProto),
};
}),
outputs: outputs.map((output) => {
assert(output.address, "missing output.address");
assertDefinedAndNotNull(output.address, "missing output.address");
assertDefinedAndNotNull(output.coins, "missing output.coins");
return {
address: output.address,
coins: checkAmount(output.coins),
coins: output.coins.map(coinFromProto),
};
}),
};
@ -105,20 +95,15 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
validatorDstAddress,
amount,
}: IMsgBeginRedelegate): MsgBeginRedelegate["value"] => {
assert(delegatorAddress, "missing delegatorAddress");
assert(validatorSrcAddress, "missing validatorSrcAddress");
assert(validatorDstAddress, "missing validatorDstAddress");
assert(amount, "missing amount");
assert(amount.amount, "missing amount.amount");
assert(amount.denom, "missing amount.denom");
assertDefinedAndNotNull(delegatorAddress, "missing delegatorAddress");
assertDefinedAndNotNull(validatorSrcAddress, "missing validatorSrcAddress");
assertDefinedAndNotNull(validatorDstAddress, "missing validatorDstAddress");
assertDefinedAndNotNull(amount, "missing amount");
return {
delegator_address: delegatorAddress,
validator_src_address: validatorSrcAddress,
validator_dst_address: validatorDstAddress,
amount: {
amount: amount.amount,
denom: amount.denom,
},
amount: coinFromProto(amount),
};
},
fromAmino: ({
@ -144,24 +129,22 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
pubkey,
value,
}: IMsgCreateValidator): MsgCreateValidator["value"] => {
assert(description, "missing description");
assert(description.moniker, "missing description.moniker");
assert(description.identity, "missing description.identity");
assert(description.website, "missing description.website");
assert(description.securityContact, "missing description.securityContact");
assert(description.details, "missing description.details");
assert(commission, "missing commission");
assert(commission.rate, "missing commission.rate");
assert(commission.maxRate, "missing commission.maxRate");
assert(commission.maxChangeRate, "missing commission.maxChangeRate");
assert(minSelfDelegation, "missing minSelfDelegation");
assert(delegatorAddress, "missing delegatorAddress");
assert(validatorAddress, "missing validatorAddress");
assert(pubkey, "missing pubkey");
assert(pubkey.value, "missing pubkey.value");
assert(value, "missing value");
assert(value.amount, "missing value.amount");
assert(value.denom, "missing value.denom");
assertDefinedAndNotNull(description, "missing description");
assertDefinedAndNotNull(description.moniker, "missing description.moniker");
assertDefinedAndNotNull(description.identity, "missing description.identity");
assertDefinedAndNotNull(description.website, "missing description.website");
assertDefinedAndNotNull(description.securityContact, "missing description.securityContact");
assertDefinedAndNotNull(description.details, "missing description.details");
assertDefinedAndNotNull(commission, "missing commission");
assertDefinedAndNotNull(commission.rate, "missing commission.rate");
assertDefinedAndNotNull(commission.maxRate, "missing commission.maxRate");
assertDefinedAndNotNull(commission.maxChangeRate, "missing commission.maxChangeRate");
assertDefinedAndNotNull(minSelfDelegation, "missing minSelfDelegation");
assertDefinedAndNotNull(delegatorAddress, "missing delegatorAddress");
assertDefinedAndNotNull(validatorAddress, "missing validatorAddress");
assertDefinedAndNotNull(pubkey, "missing pubkey");
assertDefinedAndNotNull(pubkey.value, "missing pubkey.value");
assertDefinedAndNotNull(value, "missing value");
return {
description: {
moniker: description.moniker,
@ -185,10 +168,7 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
},
prefix,
),
value: {
amount: value.amount,
denom: value.denom,
},
value: coinFromProto(value),
};
},
fromAmino: ({
@ -231,18 +211,13 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
"/cosmos.staking.v1beta1.MsgDelegate": {
aminoType: "cosmos-sdk/MsgDelegate",
toAmino: ({ delegatorAddress, validatorAddress, amount }: IMsgDelegate): MsgDelegate["value"] => {
assert(delegatorAddress, "missing delegatorAddress");
assert(validatorAddress, "missing validatorAddress");
assert(amount, "missing amount");
assert(amount.amount, "missing amount.amount");
assert(amount.denom, "missing amount.denom");
assertDefinedAndNotNull(delegatorAddress, "missing delegatorAddress");
assertDefinedAndNotNull(validatorAddress, "missing validatorAddress");
assertDefinedAndNotNull(amount, "missing amount");
return {
delegator_address: delegatorAddress,
validator_address: validatorAddress,
amount: {
amount: amount.amount,
denom: amount.denom,
},
amount: coinFromProto(amount),
};
},
fromAmino: ({ delegator_address, validator_address, amount }: MsgDelegate["value"]): IMsgDelegate => ({
@ -259,15 +234,15 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
minSelfDelegation,
validatorAddress,
}: IMsgEditValidator): MsgEditValidator["value"] => {
assert(description, "missing description");
assert(description.moniker, "missing description.moniker");
assert(description.identity, "missing description.identity");
assert(description.website, "missing description.website");
assert(description.securityContact, "missing description.securityContact");
assert(description.details, "missing description.details");
assert(commissionRate, "missing commissionRate");
assert(minSelfDelegation, "missing minSelfDelegation");
assert(validatorAddress, "missing validatorAddress");
assertDefinedAndNotNull(description, "missing description");
assertDefinedAndNotNull(description.moniker, "missing description.moniker");
assertDefinedAndNotNull(description.identity, "missing description.identity");
assertDefinedAndNotNull(description.website, "missing description.website");
assertDefinedAndNotNull(description.securityContact, "missing description.securityContact");
assertDefinedAndNotNull(description.details, "missing description.details");
assertDefinedAndNotNull(commissionRate, "missing commissionRate");
assertDefinedAndNotNull(minSelfDelegation, "missing minSelfDelegation");
assertDefinedAndNotNull(validatorAddress, "missing validatorAddress");
return {
description: {
moniker: description.moniker,
@ -302,18 +277,13 @@ function createDefaultTypes(prefix: string): Record<string, AminoConverter> {
"/cosmos.staking.v1beta1.MsgUndelegate": {
aminoType: "cosmos-sdk/MsgUndelegate",
toAmino: ({ delegatorAddress, validatorAddress, amount }: IMsgUndelegate): MsgUndelegate["value"] => {
assert(delegatorAddress, "missing delegatorAddress");
assert(validatorAddress, "missing validatorAddress");
assert(amount, "missing amount");
assert(amount.amount, "missing amount.amount");
assert(amount.denom, "missing amount.denom");
assertDefinedAndNotNull(delegatorAddress, "missing delegatorAddress");
assertDefinedAndNotNull(validatorAddress, "missing validatorAddress");
assertDefinedAndNotNull(amount, "missing amount");
return {
delegator_address: delegatorAddress,
validator_address: validatorAddress,
amount: {
amount: amount.amount,
denom: amount.denom,
},
amount: coinFromProto(amount),
};
},
fromAmino: ({

View File

@ -1,5 +1,5 @@
export * as codec from "./codec";
export { AminoTypes } from "./aminotypes";
export { AminoConverter, AminoTypes } from "./aminotypes";
export { parseRawLog } from "./logs";
export {
AuthExtension,

View File

@ -3,7 +3,7 @@ import { iavlSpec, ics23, tendermintSpec, verifyExistence, verifyNonExistence }
import { toAscii, toHex } from "@cosmjs/encoding";
import { firstEvent } from "@cosmjs/stream";
import { Client as TendermintClient, Header, NewBlockHeaderEvent, ProofOp } from "@cosmjs/tendermint-rpc";
import { arrayContentEquals, assert, assertDefined, isNonNullObject, sleep } from "@cosmjs/utils";
import { arrayContentEquals, assert, assertDefinedAndNotNull, isNonNullObject, sleep } from "@cosmjs/utils";
import { Stream } from "xstream";
type QueryExtensionSetup<P> = (base: QueryClient) => P;
@ -233,7 +233,7 @@ export class QueryClient {
// this must return the header for height+1
// throws an error if height is 0 or undefined
private async getNextHeader(height?: number): Promise<Header> {
assertDefined(height);
assertDefinedAndNotNull(height);
if (height === 0) {
throw new Error("Query returned height 0, cannot prove it");
}

View File

@ -24,6 +24,9 @@ import {
validator,
} from "./testutils.spec";
type IMsgSend = cosmos.bank.v1beta1.IMsgSend;
type IMsgDelegate = cosmos.staking.v1beta1.IMsgDelegate;
const { MsgSend } = cosmos.bank.v1beta1;
const { MsgDelegate } = cosmos.staking.v1beta1;
const { Tx } = cosmos.tx.v1beta1;
@ -126,7 +129,7 @@ describe("SigningStargateClient", () => {
});
describe("sendTokens", () => {
it("works", async () => {
it("works with direct signer", async () => {
pendingWithoutSimapp();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
@ -149,6 +152,30 @@ describe("SigningStargateClient", () => {
assert(after);
expect(after).toEqual(transferAmount[0]);
});
it("works with legacy Amino signer", async () => {
pendingWithoutSimapp();
const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
const transferAmount = coins(7890, "ucosm");
const beneficiaryAddress = makeRandomAddress();
const memo = "for dinner";
// no tokens here
const before = await client.getBalance(beneficiaryAddress, "ucosm");
expect(before).toBeNull();
// send
const result = await client.sendTokens(faucet.address0, beneficiaryAddress, transferAmount, memo);
assertIsBroadcastTxSuccess(result);
expect(result.rawLog).toBeTruthy();
// got tokens
const after = await client.getBalance(beneficiaryAddress, "ucosm");
assert(after);
expect(after).toEqual(transferAmount[0]);
});
});
describe("signAndBroadcast", () => {
@ -235,7 +262,7 @@ describe("SigningStargateClient", () => {
const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
const msgSend = {
const msgSend: IMsgSend = {
fromAddress: faucet.address0,
toAddress: makeRandomAddress(),
amount: coins(1234, "ucosm"),
@ -258,7 +285,7 @@ describe("SigningStargateClient", () => {
const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
const msgDelegate = {
const msgDelegate: IMsgDelegate = {
delegatorAddress: faucet.address0,
validatorAddress: validator.validatorAddress,
amount: coin(1234, "ustake"),

View File

@ -40,6 +40,17 @@ const { TxRaw } = cosmos.tx.v1beta1;
const defaultGasPrice = GasPrice.fromString("0.025ucosm");
const defaultGasLimits: GasLimits<CosmosFeeTable> = { send: 80000 };
function createDefaultRegistry(): Registry {
return new Registry([
["/cosmos.bank.v1beta1.MsgMultiSend", MsgMultiSend],
["/cosmos.staking.v1beta1.MsgBeginRedelegate", MsgBeginRedelegate],
["/cosmos.staking.v1beta1.MsgCreateValidator", MsgCreateValidator],
["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate],
["/cosmos.staking.v1beta1.MsgEditValidator", MsgEditValidator],
["/cosmos.staking.v1beta1.MsgUndelegate", MsgUndelegate],
]);
}
/** Use for testing only */
export interface PrivateSigningStargateClient {
readonly fees: CosmosFeeTable;
@ -49,6 +60,7 @@ export interface PrivateSigningStargateClient {
export interface SigningStargateClientOptions {
readonly registry?: Registry;
readonly aminoTypes?: AminoTypes;
readonly prefix?: string;
readonly gasPrice?: GasPrice;
readonly gasLimits?: GasLimits<CosmosFeeTable>;
}
@ -57,7 +69,7 @@ export class SigningStargateClient extends StargateClient {
private readonly fees: CosmosFeeTable;
private readonly registry: Registry;
private readonly signer: OfflineSigner;
private readonly aminoTypes;
private readonly aminoTypes: AminoTypes;
public static async connectWithSigner(
endpoint: string,
@ -75,15 +87,8 @@ export class SigningStargateClient extends StargateClient {
) {
super(tmClient);
const {
registry = new Registry([
["/cosmos.bank.v1beta1.MsgMultiSend", MsgMultiSend],
["/cosmos.staking.v1beta1.MsgBeginRedelegate", MsgBeginRedelegate],
["/cosmos.staking.v1beta1.MsgCreateValidator", MsgCreateValidator],
["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate],
["/cosmos.staking.v1beta1.MsgEditValidator", MsgEditValidator],
["/cosmos.staking.v1beta1.MsgUndelegate", MsgUndelegate],
]),
aminoTypes = new AminoTypes(),
registry = createDefaultRegistry(),
aminoTypes = new AminoTypes({ prefix: options.prefix }),
gasPrice = defaultGasPrice,
gasLimits = defaultGasLimits,
} = options;
@ -111,19 +116,19 @@ export class SigningStargateClient extends StargateClient {
}
public async signAndBroadcast(
address: string,
signerAddress: string,
messages: readonly EncodeObject[],
fee: StdFee,
memo = "",
): Promise<BroadcastTxResponse> {
const accountFromSigner = (await this.signer.getAccounts()).find(
(account: AccountData) => account.address === address,
(account: AccountData) => account.address === signerAddress,
);
if (!accountFromSigner) {
throw new Error("Failed to retrieve account from signer");
}
const pubkey = encodeSecp256k1Pubkey(accountFromSigner.pubkey);
const accountFromChain = await this.getAccount(address);
const accountFromChain = await this.getAccount(signerAddress);
if (!accountFromChain) {
throw new Error("Account not found");
}
@ -146,7 +151,7 @@ export class SigningStargateClient extends StargateClient {
if (isOfflineDirectSigner(this.signer)) {
const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence);
const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber);
const { signature, signed } = await this.signer.signDirect(address, signDoc);
const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc);
const txRaw = TxRaw.create({
bodyBytes: signed.bodyBytes,
authInfoBytes: signed.authInfoBytes,
@ -160,7 +165,7 @@ export class SigningStargateClient extends StargateClient {
const signMode = cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
const msgs = messages.map((msg) => this.aminoTypes.toAmino(msg));
const signDoc = makeSignDocAmino(msgs, fee, chainId, memo, accountNumber, sequence);
const { signature, signed } = await this.signer.signAmino(address, signDoc);
const { signature, signed } = await this.signer.signAmino(signerAddress, signDoc);
const signedTxBody = {
messages: signed.msgs.map((msg) => this.aminoTypes.fromAmino(msg)),
memo: signed.memo,

View File

@ -18,7 +18,7 @@ import {
Client as TendermintClient,
DateTime,
} from "@cosmjs/tendermint-rpc";
import { assert, assertDefined } from "@cosmjs/utils";
import { assert, assertDefinedAndNotNull } from "@cosmjs/utils";
import Long from "long";
import { cosmos } from "./codec";
@ -110,10 +110,8 @@ export function accountFromProto(input: IBaseAccount): Account {
}
export function coinFromProto(input: ICoin): Coin {
assertDefined(input.amount);
assertDefined(input.denom);
assert(input.amount !== null);
assert(input.denom !== null);
assertDefinedAndNotNull(input.amount);
assertDefinedAndNotNull(input.denom);
return {
amount: input.amount,
denom: input.denom,

View File

@ -1,5 +1,5 @@
export * as codec from "./codec";
export { AminoTypes } from "./aminotypes";
export { AminoConverter, AminoTypes } from "./aminotypes";
export { parseRawLog } from "./logs";
export {
AuthExtension,

View File

@ -10,6 +10,7 @@ export interface PrivateSigningStargateClient {
export interface SigningStargateClientOptions {
readonly registry?: Registry;
readonly aminoTypes?: AminoTypes;
readonly prefix?: string;
readonly gasPrice?: GasPrice;
readonly gasLimits?: GasLimits<CosmosFeeTable>;
}
@ -31,7 +32,7 @@ export declare class SigningStargateClient extends StargateClient {
memo?: string,
): Promise<BroadcastTxResponse>;
signAndBroadcast(
address: string,
signerAddress: string,
messages: readonly EncodeObject[],
fee: StdFee,
memo?: string,

View File

@ -1,52 +1,54 @@
import { assertDefined } from "./assert";
import { assertDefinedAndNotNull } from "./assert";
describe("assert", () => {
describe("assertDefined", () => {
describe("assertDefinedAndNotNull", () => {
it("passes for simple values", () => {
{
const value: number | undefined = 123;
assertDefined(value);
const value: number | undefined | null = 123;
assertDefinedAndNotNull(value);
expect(value).toEqual(123);
}
{
const value: string | undefined = "abc";
assertDefined(value);
const value: string | undefined | null = "abc";
assertDefinedAndNotNull(value);
expect(value).toEqual("abc");
}
});
it("passes for falsy values", () => {
{
const value: number | undefined = 0;
assertDefined(value);
const value: number | undefined | null = 0;
assertDefinedAndNotNull(value);
expect(value).toEqual(0);
}
{
const value: string | undefined = "";
assertDefined(value);
const value: string | undefined | null = "";
assertDefinedAndNotNull(value);
expect(value).toEqual("");
}
{
const value: null | undefined = null;
assertDefined(value);
expect(value).toEqual(null);
}
});
it("throws for undefined values", () => {
{
const value: number | undefined = undefined;
expect(() => assertDefined(value)).toThrowError("value is undefined");
const value: number | undefined | null = undefined;
expect(() => assertDefinedAndNotNull(value)).toThrowError("value is undefined or null");
}
{
let value: string | undefined;
expect(() => assertDefined(value)).toThrowError("value is undefined");
let value: string | undefined | null;
expect(() => assertDefinedAndNotNull(value)).toThrowError("value is undefined or null");
}
});
it("throws for null values", () => {
const value: number | undefined | null = null;
expect(() => assertDefinedAndNotNull(value)).toThrowError("value is undefined or null");
});
it("throws with custom message", () => {
const value: number | undefined = undefined;
expect(() => assertDefined(value, "Bug in the data source")).toThrowError("Bug in the data source");
expect(() => assertDefinedAndNotNull(value, "Bug in the data source")).toThrowError(
"Bug in the data source",
);
});
});
});

View File

@ -5,8 +5,8 @@ export function assert(condition: any, msg?: string): asserts condition {
}
}
export function assertDefined<T>(value: T | undefined, msg?: string): asserts value is T {
if (value === undefined) {
throw new Error(msg || "value is undefined");
export function assertDefinedAndNotNull<T>(value: T | undefined | null, msg?: string): asserts value is T {
if (value === undefined || value === null) {
throw new Error(msg ?? "value is undefined or null");
}
}

View File

@ -1,4 +1,4 @@
export { arrayContentEquals } from "./arrays";
export { assert, assertDefined } from "./assert";
export { assert, assertDefinedAndNotNull } from "./assert";
export { sleep } from "./sleep";
export { isNonNullObject, isUint8Array } from "./typechecks";

View File

@ -1,2 +1,5 @@
export declare function assert(condition: any, msg?: string): asserts condition;
export declare function assertDefined<T>(value: T | undefined, msg?: string): asserts value is T;
export declare function assertDefinedAndNotNull<T>(
value: T | undefined | null,
msg?: string,
): asserts value is T;

View File

@ -1,4 +1,4 @@
export { arrayContentEquals } from "./arrays";
export { assert, assertDefined } from "./assert";
export { assert, assertDefinedAndNotNull } from "./assert";
export { sleep } from "./sleep";
export { isNonNullObject, isUint8Array } from "./typechecks";