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 - @cosmjs/tendermint-rpc: Remove types `BlockHash`, `TxBytes` and `TxHash`. Use
`Uint8Array` instead. `Uint8Array` instead.
### Added
- @cosmjs/utils: Added `assertDefinedAndNotNull`.
### Removed
- @cosmjs/utils: `assertDefined` removed in favour of `assertDefinedAndNotNull`.
## 0.23.2 (2021-01-06) ## 0.23.2 (2021-01-06)
### Security ### 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, parseRawLog,
SigningStargateClient, SigningStargateClient,
} from "@cosmjs/stargate"; } from "@cosmjs/stargate";
import { assert, assertDefined } from "@cosmjs/utils"; import { assert, assertDefinedAndNotNull } from "@cosmjs/utils";
import Long from "long"; import Long from "long";
import { cosmwasm } from "../codec"; import { cosmwasm } from "../codec";
@ -395,7 +395,7 @@ describe("WasmExtension", () => {
expect(codeId).toBeGreaterThanOrEqual(1); expect(codeId).toBeGreaterThanOrEqual(1);
expect(codeId).toBeLessThanOrEqual(200); expect(codeId).toBeLessThanOrEqual(200);
assertDefined(result.data); assertDefinedAndNotNull(result.data);
const msgData = fromOneElementArray(result.data); const msgData = fromOneElementArray(result.data);
expect(msgData.msgType).toEqual("store-code"); expect(msgData.msgType).toEqual("store-code");
expect(MsgStoreCodeResponse.decode(msgData.data!)).toEqual( expect(MsgStoreCodeResponse.decode(msgData.data!)).toEqual(
@ -415,7 +415,7 @@ describe("WasmExtension", () => {
const amountAttr = logs.findAttribute(parsedLogs, "transfer", "amount"); const amountAttr = logs.findAttribute(parsedLogs, "transfer", "amount");
expect(amountAttr.value).toEqual("1234ucosm,321ustake"); expect(amountAttr.value).toEqual("1234ucosm,321ustake");
assertDefined(result.data); assertDefinedAndNotNull(result.data);
const msgData = fromOneElementArray(result.data); const msgData = fromOneElementArray(result.data);
expect(msgData.msgType).toEqual("instantiate"); expect(msgData.msgType).toEqual("instantiate");
expect(MsgInstantiateContractResponse.decode(msgData.data!)).toEqual( expect(MsgInstantiateContractResponse.decode(msgData.data!)).toEqual(
@ -441,7 +441,7 @@ describe("WasmExtension", () => {
value: beneficiaryAddress, value: beneficiaryAddress,
}); });
assertDefined(result.data); assertDefinedAndNotNull(result.data);
const msgData = fromOneElementArray(result.data); const msgData = fromOneElementArray(result.data);
expect(msgData.msgType).toEqual("execute"); expect(msgData.msgType).toEqual("execute");
expect(MsgExecuteContractResponse.decode(msgData.data!)).toEqual( expect(MsgExecuteContractResponse.decode(msgData.data!)).toEqual(

View File

@ -2,12 +2,21 @@
import { UploadMeta } from "@cosmjs/cosmwasm-launchpad"; import { UploadMeta } from "@cosmjs/cosmwasm-launchpad";
import { sha256 } from "@cosmjs/crypto"; import { sha256 } from "@cosmjs/crypto";
import { toHex } from "@cosmjs/encoding"; import { toHex } from "@cosmjs/encoding";
import { coin, coins, GasPrice } from "@cosmjs/launchpad"; import {
import { DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; coin,
import { assertIsBroadcastTxSuccess, codec } from "@cosmjs/stargate"; 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 { assert, sleep } from "@cosmjs/utils";
import Long from "long"; import Long from "long";
import pako from "pako";
import { Message } from "protobufjs";
import { cosmwasm } from "./codec";
import { PrivateSigningCosmWasmClient, SigningCosmWasmClient } from "./signingcosmwasmclient"; import { PrivateSigningCosmWasmClient, SigningCosmWasmClient } from "./signingcosmwasmclient";
import { import {
alice, alice,
@ -15,12 +24,18 @@ import {
makeRandomAddress, makeRandomAddress,
makeWasmClient, makeWasmClient,
ModifyingDirectSecp256k1HdWallet, ModifyingDirectSecp256k1HdWallet,
ModifyingSecp256k1HdWallet,
pendingWithoutWasmd, pendingWithoutWasmd,
unused, unused,
validator, validator,
wasmd, wasmd,
} from "./testutils.spec"; } 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 { MsgDelegate } = codec.cosmos.staking.v1beta1;
const { Tx } = codec.cosmos.tx.v1beta1; const { Tx } = codec.cosmos.tx.v1beta1;
@ -29,15 +44,30 @@ describe("SigningCosmWasmClient", () => {
it("can be constructed", async () => { it("can be constructed", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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(); 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 () => { it("can be constructed with custom gas price", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const gasPrice = GasPrice.fromString("3.14utest"); const options = {
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, { gasPrice }); prefix: wasmd.prefix,
gasPrice: GasPrice.fromString("3.14utest"),
};
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const openedClient = (client as unknown) as PrivateSigningCosmWasmClient; const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.fees).toEqual({ expect(openedClient.fees).toEqual({
upload: { upload: {
@ -70,10 +100,13 @@ describe("SigningCosmWasmClient", () => {
it("can be constructed with custom gas limits", async () => { it("can be constructed with custom gas limits", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const gasLimits = { const options = {
send: 160000, 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; const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.fees).toEqual({ expect(openedClient.fees).toEqual({
upload: { upload: {
@ -106,14 +139,14 @@ describe("SigningCosmWasmClient", () => {
it("can be constructed with custom gas price and gas limits", async () => { it("can be constructed with custom gas price and gas limits", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix);
const gasPrice = GasPrice.fromString("3.14utest"); const options = {
const gasLimits = { prefix: wasmd.prefix,
send: 160000, gasPrice: GasPrice.fromString("3.14utest"),
gasLimits: {
send: 160000,
},
}; };
const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, { const client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
gasPrice,
gasLimits,
});
const openedClient = (client as unknown) as PrivateSigningCosmWasmClient; const openedClient = (client as unknown) as PrivateSigningCosmWasmClient;
expect(openedClient.fees).toEqual({ expect(openedClient.fees).toEqual({
upload: { upload: {
@ -148,7 +181,8 @@ describe("SigningCosmWasmClient", () => {
it("works", async () => { it("works", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 wasm = getHackatom().data;
const { const {
codeId, codeId,
@ -167,7 +201,8 @@ describe("SigningCosmWasmClient", () => {
it("can set builder and source", async () => { it("can set builder and source", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 hackatom = getHackatom();
const meta: UploadMeta = { const meta: UploadMeta = {
source: "https://crates.io/api/v1/crates/cw-nameservice/0.1.0/download", 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 () => { it("works with transfer amount", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 { codeId } = await client.upload(alice.address0, getHackatom().data);
const transferAmount = [coin(1234, "ucosm"), coin(321, "ustake")]; const transferAmount = [coin(1234, "ucosm"), coin(321, "ustake")];
const beneficiaryAddress = makeRandomAddress(); const beneficiaryAddress = makeRandomAddress();
@ -211,7 +247,8 @@ describe("SigningCosmWasmClient", () => {
it("works with admin", async () => { it("works with admin", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 { codeId } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress(); const beneficiaryAddress = makeRandomAddress();
const { contractAddress } = await client.instantiate( const { contractAddress } = await client.instantiate(
@ -233,7 +270,8 @@ describe("SigningCosmWasmClient", () => {
it("can instantiate one code multiple times", async () => { it("can instantiate one code multiple times", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 { codeId } = await client.upload(alice.address0, getHackatom().data);
const contractAddress1 = await client.instantiate( const contractAddress1 = await client.instantiate(
alice.address0, alice.address0,
@ -261,7 +299,8 @@ describe("SigningCosmWasmClient", () => {
it("can update an admin", async () => { it("can update an admin", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 { codeId } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress(); const beneficiaryAddress = makeRandomAddress();
const { contractAddress } = await client.instantiate( const { contractAddress } = await client.instantiate(
@ -296,7 +335,8 @@ describe("SigningCosmWasmClient", () => {
it("can clear an admin", async () => { it("can clear an admin", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 { codeId } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress(); const beneficiaryAddress = makeRandomAddress();
const { contractAddress } = await client.instantiate( const { contractAddress } = await client.instantiate(
@ -331,7 +371,8 @@ describe("SigningCosmWasmClient", () => {
it("can can migrate from one code ID to another", async () => { it("can can migrate from one code ID to another", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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: codeId1 } = await client.upload(alice.address0, getHackatom().data);
const { codeId: codeId2 } = await client.upload(alice.address0, getHackatom().data); const { codeId: codeId2 } = await client.upload(alice.address0, getHackatom().data);
const beneficiaryAddress = makeRandomAddress(); const beneficiaryAddress = makeRandomAddress();
@ -371,7 +412,8 @@ describe("SigningCosmWasmClient", () => {
it("works", async () => { it("works", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 { codeId } = await client.upload(alice.address0, getHackatom().data);
// instantiate // instantiate
const transferAmount = [coin(233444, "ucosm"), coin(5454, "ustake")]; const transferAmount = [coin(233444, "ucosm"), coin(5454, "ustake")];
@ -411,10 +453,36 @@ describe("SigningCosmWasmClient", () => {
}); });
describe("sendTokens", () => { describe("sendTokens", () => {
it("works", async () => { it("works with direct signer", async () => {
pendingWithoutWasmd(); pendingWithoutWasmd();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(alice.mnemonic, undefined, wasmd.prefix); 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 transferAmount = coins(7890, "ucosm");
const beneficiaryAddress = makeRandomAddress(); const beneficiaryAddress = makeRandomAddress();
@ -444,7 +512,7 @@ describe("SigningCosmWasmClient", () => {
const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate";
const registry = new Registry(); const registry = new Registry();
registry.register(msgDelegateTypeUrl, MsgDelegate); 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 client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msg = MsgDelegate.create({ const msg = MsgDelegate.create({
@ -475,7 +543,7 @@ describe("SigningCosmWasmClient", () => {
const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate"; const msgDelegateTypeUrl = "/cosmos.staking.v1beta1.MsgDelegate";
const registry = new Registry(); const registry = new Registry();
registry.register(msgDelegateTypeUrl, MsgDelegate); 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 client = await SigningCosmWasmClient.connectWithSigner(wasmd.endpoint, wallet, options);
const msg = MsgDelegate.create({ const msg = MsgDelegate.create({
@ -506,5 +574,193 @@ describe("SigningCosmWasmClient", () => {
expect(tx.authInfo!.fee!.gasLimit!.toNumber()).toEqual(333333); 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, GasLimits,
GasPrice, GasPrice,
logs, logs,
makeSignDoc as makeSignDocAmino,
StdFee, StdFee,
} from "@cosmjs/launchpad"; } from "@cosmjs/launchpad";
import { Int53, Uint53 } from "@cosmjs/math"; import { Int53, Uint53 } from "@cosmjs/math";
import { import {
EncodeObject, EncodeObject,
encodePubkey, encodePubkey,
isOfflineDirectSigner,
makeAuthInfoBytes, makeAuthInfoBytes,
makeSignDoc, makeSignDoc,
OfflineDirectSigner, OfflineSigner,
Registry, Registry,
} from "@cosmjs/proto-signing"; } from "@cosmjs/proto-signing";
import { import {
AminoTypes,
BroadcastTxFailure, BroadcastTxFailure,
BroadcastTxResponse, BroadcastTxResponse,
codec, codec,
@ -43,10 +46,20 @@ import { adaptor34, Client as TendermintClient } from "@cosmjs/tendermint-rpc";
import Long from "long"; import Long from "long";
import pako from "pako"; import pako from "pako";
import { cosmWasmTypes } from "./aminotypes";
import { cosmwasm } from "./codec"; import { cosmwasm } from "./codec";
import { CosmWasmClient } from "./cosmwasmclient"; import { CosmWasmClient } from "./cosmwasmclient";
const { SignMode } = codec.cosmos.tx.signing.v1beta1;
const { TxRaw } = codec.cosmos.tx.v1beta1; const { TxRaw } = codec.cosmos.tx.v1beta1;
const { MsgMultiSend } = codec.cosmos.bank.v1beta1;
const {
MsgBeginRedelegate,
MsgCreateValidator,
MsgDelegate,
MsgEditValidator,
MsgUndelegate,
} = codec.cosmos.staking.v1beta1;
const { const {
MsgClearAdmin, MsgClearAdmin,
MsgExecuteContract, MsgExecuteContract,
@ -81,6 +94,12 @@ function createBroadcastTxErrorMessage(result: BroadcastTxFailure): string {
function createDefaultRegistry(): Registry { function createDefaultRegistry(): Registry {
return new 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.MsgClearAdmin", MsgClearAdmin],
["/cosmwasm.wasm.v1beta1.MsgExecuteContract", MsgExecuteContract], ["/cosmwasm.wasm.v1beta1.MsgExecuteContract", MsgExecuteContract],
["/cosmwasm.wasm.v1beta1.MsgMigrateContract", MsgMigrateContract], ["/cosmwasm.wasm.v1beta1.MsgMigrateContract", MsgMigrateContract],
@ -92,6 +111,8 @@ function createDefaultRegistry(): Registry {
export interface SigningCosmWasmClientOptions { export interface SigningCosmWasmClientOptions {
readonly registry?: Registry; readonly registry?: Registry;
readonly aminoTypes?: AminoTypes;
readonly prefix?: string;
readonly gasPrice?: GasPrice; readonly gasPrice?: GasPrice;
readonly gasLimits?: GasLimits<CosmosFeeTable>; readonly gasLimits?: GasLimits<CosmosFeeTable>;
} }
@ -99,16 +120,18 @@ export interface SigningCosmWasmClientOptions {
/** Use for testing only */ /** Use for testing only */
export interface PrivateSigningCosmWasmClient { export interface PrivateSigningCosmWasmClient {
readonly fees: CosmWasmFeeTable; readonly fees: CosmWasmFeeTable;
readonly registry: Registry;
} }
export class SigningCosmWasmClient extends CosmWasmClient { export class SigningCosmWasmClient extends CosmWasmClient {
private readonly fees: CosmosFeeTable; private readonly fees: CosmosFeeTable;
private readonly registry: Registry; private readonly registry: Registry;
private readonly signer: OfflineDirectSigner; private readonly signer: OfflineSigner;
private readonly aminoTypes: AminoTypes;
public static async connectWithSigner( public static async connectWithSigner(
endpoint: string, endpoint: string,
signer: OfflineDirectSigner, signer: OfflineSigner,
options: SigningCosmWasmClientOptions = {}, options: SigningCosmWasmClientOptions = {},
): Promise<SigningCosmWasmClient> { ): Promise<SigningCosmWasmClient> {
const tmClient = await TendermintClient.connect(endpoint, adaptor34); const tmClient = await TendermintClient.connect(endpoint, adaptor34);
@ -117,17 +140,19 @@ export class SigningCosmWasmClient extends CosmWasmClient {
private constructor( private constructor(
tmClient: TendermintClient, tmClient: TendermintClient,
signer: OfflineDirectSigner, signer: OfflineSigner,
options: SigningCosmWasmClientOptions, options: SigningCosmWasmClientOptions,
) { ) {
super(tmClient); super(tmClient);
const { const {
registry = createDefaultRegistry(), registry = createDefaultRegistry(),
aminoTypes = new AminoTypes({ additions: cosmWasmTypes, prefix: options.prefix }),
gasPrice = defaultGasPrice, gasPrice = defaultGasPrice,
gasLimits = defaultGasLimits, gasLimits = defaultGasLimits,
} = options; } = options;
this.fees = buildFeeTable<CosmosFeeTable>(gasPrice, defaultGasLimits, gasLimits); this.fees = buildFeeTable<CosmosFeeTable>(gasPrice, defaultGasLimits, gasLimits);
this.registry = registry; this.registry = registry;
this.aminoTypes = aminoTypes;
this.signer = signer; this.signer = signer;
} }
@ -355,12 +380,44 @@ export class SigningCosmWasmClient extends CosmWasmClient {
}); });
const gasLimit = Int53.fromString(fee.gas).toNumber(); const gasLimit = Int53.fromString(fee.gas).toNumber();
const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence); if (isOfflineDirectSigner(this.signer)) {
const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); const authInfoBytes = makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence);
const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); 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({ const txRaw = TxRaw.create({
bodyBytes: signed.bodyBytes, bodyBytes: signedTxBodyBytes,
authInfoBytes: signed.authInfoBytes, authInfoBytes: signedAuthInfoBytes,
signatures: [fromBase64(signature.signature)], signatures: [fromBase64(signature.signature)],
}); });
const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish()); const signedTx = Uint8Array.from(TxRaw.encode(txRaw).finish());

View File

@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto"; import { Bip39, EnglishMnemonic, Random, Secp256k1, Slip10, Slip10Curve } from "@cosmjs/crypto";
import { Bech32, fromBase64 } from "@cosmjs/encoding"; 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 { DirectSecp256k1HdWallet, DirectSignResponse, makeAuthInfoBytes } from "@cosmjs/proto-signing";
import { import {
AuthExtension, AuthExtension,
@ -205,6 +211,41 @@ export async function makeWasmClient(
return QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension, setupWasmExtension); 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 * 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, UploadResult,
} from "@cosmjs/cosmwasm-launchpad"; } from "@cosmjs/cosmwasm-launchpad";
import { Coin, CosmosFeeTable, GasLimits, GasPrice, StdFee } from "@cosmjs/launchpad"; import { Coin, CosmosFeeTable, GasLimits, GasPrice, StdFee } from "@cosmjs/launchpad";
import { EncodeObject, OfflineDirectSigner, Registry } from "@cosmjs/proto-signing"; import { EncodeObject, OfflineSigner, Registry } from "@cosmjs/proto-signing";
import { BroadcastTxResponse } from "@cosmjs/stargate"; import { AminoTypes, BroadcastTxResponse } from "@cosmjs/stargate";
import { CosmWasmClient } from "./cosmwasmclient"; import { CosmWasmClient } from "./cosmwasmclient";
export interface SigningCosmWasmClientOptions { export interface SigningCosmWasmClientOptions {
readonly registry?: Registry; readonly registry?: Registry;
readonly aminoTypes?: AminoTypes;
readonly prefix?: string;
readonly gasPrice?: GasPrice; readonly gasPrice?: GasPrice;
readonly gasLimits?: GasLimits<CosmosFeeTable>; readonly gasLimits?: GasLimits<CosmosFeeTable>;
} }
/** Use for testing only */ /** Use for testing only */
export interface PrivateSigningCosmWasmClient { export interface PrivateSigningCosmWasmClient {
readonly fees: CosmWasmFeeTable; readonly fees: CosmWasmFeeTable;
readonly registry: Registry;
} }
export declare class SigningCosmWasmClient extends CosmWasmClient { export declare class SigningCosmWasmClient extends CosmWasmClient {
private readonly fees; private readonly fees;
private readonly registry; private readonly registry;
private readonly signer; private readonly signer;
private readonly aminoTypes;
static connectWithSigner( static connectWithSigner(
endpoint: string, endpoint: string,
signer: OfflineDirectSigner, signer: OfflineSigner,
options?: SigningCosmWasmClientOptions, options?: SigningCosmWasmClientOptions,
): Promise<SigningCosmWasmClient>; ): Promise<SigningCosmWasmClient>;
private constructor(); private constructor();

View File

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

View File

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

View File

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

View File

@ -24,6 +24,9 @@ import {
validator, validator,
} from "./testutils.spec"; } from "./testutils.spec";
type IMsgSend = cosmos.bank.v1beta1.IMsgSend;
type IMsgDelegate = cosmos.staking.v1beta1.IMsgDelegate;
const { MsgSend } = cosmos.bank.v1beta1; const { MsgSend } = cosmos.bank.v1beta1;
const { MsgDelegate } = cosmos.staking.v1beta1; const { MsgDelegate } = cosmos.staking.v1beta1;
const { Tx } = cosmos.tx.v1beta1; const { Tx } = cosmos.tx.v1beta1;
@ -126,7 +129,7 @@ describe("SigningStargateClient", () => {
}); });
describe("sendTokens", () => { describe("sendTokens", () => {
it("works", async () => { it("works with direct signer", async () => {
pendingWithoutSimapp(); pendingWithoutSimapp();
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
@ -149,6 +152,30 @@ describe("SigningStargateClient", () => {
assert(after); assert(after);
expect(after).toEqual(transferAmount[0]); 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", () => { describe("signAndBroadcast", () => {
@ -235,7 +262,7 @@ describe("SigningStargateClient", () => {
const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
const msgSend = { const msgSend: IMsgSend = {
fromAddress: faucet.address0, fromAddress: faucet.address0,
toAddress: makeRandomAddress(), toAddress: makeRandomAddress(),
amount: coins(1234, "ucosm"), amount: coins(1234, "ucosm"),
@ -258,7 +285,7 @@ describe("SigningStargateClient", () => {
const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic); const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic);
const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet); const client = await SigningStargateClient.connectWithSigner(simapp.tendermintUrl, wallet);
const msgDelegate = { const msgDelegate: IMsgDelegate = {
delegatorAddress: faucet.address0, delegatorAddress: faucet.address0,
validatorAddress: validator.validatorAddress, validatorAddress: validator.validatorAddress,
amount: coin(1234, "ustake"), amount: coin(1234, "ustake"),

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +1,54 @@
import { assertDefined } from "./assert"; import { assertDefinedAndNotNull } from "./assert";
describe("assert", () => { describe("assert", () => {
describe("assertDefined", () => { describe("assertDefinedAndNotNull", () => {
it("passes for simple values", () => { it("passes for simple values", () => {
{ {
const value: number | undefined = 123; const value: number | undefined | null = 123;
assertDefined(value); assertDefinedAndNotNull(value);
expect(value).toEqual(123); expect(value).toEqual(123);
} }
{ {
const value: string | undefined = "abc"; const value: string | undefined | null = "abc";
assertDefined(value); assertDefinedAndNotNull(value);
expect(value).toEqual("abc"); expect(value).toEqual("abc");
} }
}); });
it("passes for falsy values", () => { it("passes for falsy values", () => {
{ {
const value: number | undefined = 0; const value: number | undefined | null = 0;
assertDefined(value); assertDefinedAndNotNull(value);
expect(value).toEqual(0); expect(value).toEqual(0);
} }
{ {
const value: string | undefined = ""; const value: string | undefined | null = "";
assertDefined(value); assertDefinedAndNotNull(value);
expect(value).toEqual(""); expect(value).toEqual("");
} }
{
const value: null | undefined = null;
assertDefined(value);
expect(value).toEqual(null);
}
}); });
it("throws for undefined values", () => { it("throws for undefined values", () => {
{ {
const value: number | undefined = undefined; const value: number | undefined | null = undefined;
expect(() => assertDefined(value)).toThrowError("value is undefined"); expect(() => assertDefinedAndNotNull(value)).toThrowError("value is undefined or null");
} }
{ {
let value: string | undefined; let value: string | undefined | null;
expect(() => assertDefined(value)).toThrowError("value is undefined"); 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", () => { it("throws with custom message", () => {
const value: number | undefined = undefined; 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 { export function assertDefinedAndNotNull<T>(value: T | undefined | null, msg?: string): asserts value is T {
if (value === undefined) { if (value === undefined || value === null) {
throw new Error(msg || "value is undefined"); throw new Error(msg ?? "value is undefined or null");
} }
} }

View File

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

View File

@ -1,2 +1,5 @@
export declare function assert(condition: any, msg?: string): asserts condition; 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 { arrayContentEquals } from "./arrays";
export { assert, assertDefined } from "./assert"; export { assert, assertDefinedAndNotNull } from "./assert";
export { sleep } from "./sleep"; export { sleep } from "./sleep";
export { isNonNullObject, isUint8Array } from "./typechecks"; export { isNonNullObject, isUint8Array } from "./typechecks";