Merge pull request #39 from confio/empower-sdk
Upload wasm code using plain @cosmwasm/sdk
This commit is contained in:
commit
f7d63b7037
@ -5,7 +5,7 @@ workflows:
|
||||
jobs:
|
||||
- build
|
||||
- lint
|
||||
- faucet_docker
|
||||
# - faucet_docker
|
||||
- test
|
||||
|
||||
jobs:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { marshalTx, unmarshalTx } from "@cosmwasm/sdk";
|
||||
import { makeSignBytes, marshalTx, types, unmarshalTx } from "@cosmwasm/sdk";
|
||||
import {
|
||||
Address,
|
||||
ChainId,
|
||||
@ -14,7 +14,6 @@ import {
|
||||
TxCodec,
|
||||
UnsignedTransaction,
|
||||
} from "@iov/bcp";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { CosmosBech32Prefix, isValidAddress, pubkeyToAddress } from "./address";
|
||||
import { Caip5 } from "./caip5";
|
||||
@ -22,26 +21,6 @@ import { parseTx } from "./decode";
|
||||
import { buildSignedTx, buildUnsignedTx } from "./encode";
|
||||
import { nonceToAccountNumber, nonceToSequence, TokenInfos } from "./types";
|
||||
|
||||
const { toUtf8 } = Encoding;
|
||||
|
||||
function sortJson(json: any): any {
|
||||
if (typeof json !== "object" || json === null) {
|
||||
return json;
|
||||
}
|
||||
if (Array.isArray(json)) {
|
||||
return json.map(sortJson);
|
||||
}
|
||||
const sortedKeys = Object.keys(json).sort();
|
||||
const result = sortedKeys.reduce(
|
||||
(accumulator, key) => ({
|
||||
...accumulator,
|
||||
[key]: sortJson(json[key]),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export class CosmWasmCodec implements TxCodec {
|
||||
private readonly prefix: CosmosBech32Prefix;
|
||||
private readonly tokens: TokenInfos;
|
||||
@ -52,18 +31,19 @@ export class CosmWasmCodec implements TxCodec {
|
||||
}
|
||||
|
||||
public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob {
|
||||
const memo = (unsigned as any).memo;
|
||||
const built = buildUnsignedTx(unsigned, this.tokens);
|
||||
|
||||
const signMsg = sortJson({
|
||||
account_number: nonceToAccountNumber(nonce).toString(),
|
||||
chain_id: Caip5.decode(unsigned.chainId),
|
||||
fee: (built.value as any).fee,
|
||||
memo: memo,
|
||||
msgs: (built.value as any).msg,
|
||||
sequence: nonceToSequence(nonce).toString(),
|
||||
});
|
||||
const signBytes = toUtf8(JSON.stringify(signMsg));
|
||||
const nonceInfo: types.NonceInfo = {
|
||||
account_number: nonceToAccountNumber(nonce),
|
||||
sequence: nonceToSequence(nonce),
|
||||
};
|
||||
const signBytes = makeSignBytes(
|
||||
built.value.msg,
|
||||
built.value.fee,
|
||||
Caip5.decode(unsigned.chainId),
|
||||
built.value.memo || "",
|
||||
nonceInfo,
|
||||
);
|
||||
|
||||
return {
|
||||
bytes: signBytes as SignableBytes,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { RestClient, TxsResponse, unmarshalTx } from "@cosmwasm/sdk";
|
||||
import { RestClient, TxsResponse, types, unmarshalTx } from "@cosmwasm/sdk";
|
||||
import {
|
||||
Account,
|
||||
AccountQuery,
|
||||
@ -305,8 +305,19 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
response: TxsResponse,
|
||||
chainId: ChainId,
|
||||
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
|
||||
const sender = (response.tx.value as any).msg[0].value.from_address;
|
||||
const accountForHeight = await this.restClient.authAccounts(sender, response.height);
|
||||
const firstMsg = response.tx.value.msg.find(() => true);
|
||||
if (!firstMsg) throw new Error("Got transaction without a first message. What is going on here?");
|
||||
|
||||
let senderAddress: string;
|
||||
if (types.isMsgSend(firstMsg)) {
|
||||
senderAddress = firstMsg.value.from_address;
|
||||
} else if (types.isMsgStoreCode(firstMsg)) {
|
||||
senderAddress = firstMsg.value.sender;
|
||||
} else {
|
||||
throw new Error(`Got unsupported type of message: ${firstMsg.type}`);
|
||||
}
|
||||
|
||||
const accountForHeight = await this.restClient.authAccounts(senderAddress, response.height);
|
||||
// this is technically not the proper nonce. maybe this causes issues for sig validation?
|
||||
// leaving for now unless it causes issues
|
||||
const sequence = (accountForHeight.result.value.sequence - 1) as Nonce;
|
||||
|
||||
@ -72,24 +72,27 @@ export function decodeAmount(tokens: TokenInfos, coin: types.Coin): Amount {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseMsg(msg: types.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction {
|
||||
if (msg.type !== "cosmos-sdk/MsgSend") {
|
||||
throw new Error("Unknown message type in transaction");
|
||||
export function parseMsg(msg: types.Msg, chainId: ChainId, tokens: TokenInfos): UnsignedTransaction {
|
||||
if (types.isMsgSend(msg)) {
|
||||
if (msg.value.amount.length !== 1) {
|
||||
throw new Error("Only MsgSend with one amount is supported");
|
||||
}
|
||||
const send: SendTransaction = {
|
||||
kind: "bcp/send",
|
||||
chainId: chainId,
|
||||
sender: msg.value.from_address as Address,
|
||||
recipient: msg.value.to_address as Address,
|
||||
amount: decodeAmount(tokens, msg.value.amount[0]),
|
||||
};
|
||||
return send;
|
||||
} else {
|
||||
// Unknown transaction type
|
||||
const unknown = {
|
||||
chainId: chainId,
|
||||
kind: "bcp/unknown",
|
||||
};
|
||||
return unknown;
|
||||
}
|
||||
if (!(msg.value as types.MsgSend).from_address) {
|
||||
throw new Error("Only MsgSend is supported");
|
||||
}
|
||||
const msgValue = msg.value as types.MsgSend;
|
||||
if (msgValue.amount.length !== 1) {
|
||||
throw new Error("Only MsgSend with one amount is supported");
|
||||
}
|
||||
return {
|
||||
kind: "bcp/send",
|
||||
chainId: chainId,
|
||||
sender: msgValue.from_address as Address,
|
||||
recipient: msgValue.to_address as Address,
|
||||
amount: decodeAmount(tokens, msgValue.amount[0]),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseFee(fee: types.StdFee, tokens: TokenInfos): Fee {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { types } from "@cosmwasm/sdk";
|
||||
import { encodeSecp256k1Signature, types } from "@cosmwasm/sdk";
|
||||
import {
|
||||
Algorithm,
|
||||
Amount,
|
||||
@ -10,7 +10,6 @@ import {
|
||||
SignedTransaction,
|
||||
UnsignedTransaction,
|
||||
} from "@iov/bcp";
|
||||
import { Secp256k1 } from "@iov/crypto";
|
||||
import { Decimal, Encoding } from "@iov/encoding";
|
||||
|
||||
import { TokenInfos } from "./types";
|
||||
@ -72,14 +71,12 @@ export function encodeFee(fee: Fee, tokens: TokenInfos): types.StdFee {
|
||||
}
|
||||
|
||||
export function encodeFullSignature(fullSignature: FullSignature): types.StdSignature {
|
||||
return {
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: toBase64(Secp256k1.compressPubkey(fullSignature.pubkey.data)),
|
||||
},
|
||||
// Recovery seems to be unused
|
||||
signature: toBase64(Secp256k1.trimRecoveryByte(fullSignature.signature)),
|
||||
};
|
||||
switch (fullSignature.pubkey.algo) {
|
||||
case Algorithm.Secp256k1:
|
||||
return encodeSecp256k1Signature(fullSignature.pubkey.data, fullSignature.signature);
|
||||
default:
|
||||
throw new Error("Unsupported signing algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
export function buildUnsignedTx(tx: UnsignedTransaction, tokens: TokenInfos): types.AminoTx {
|
||||
|
||||
@ -23,13 +23,9 @@ const maxAcct = 1 << 23;
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
const maxSeq = 1 << 20;
|
||||
|
||||
// NonceInfo is the data we need from account to create a nonce
|
||||
// Use this so no confusion about order of arguments
|
||||
export type NonceInfo = Pick<types.BaseAccount, "account_number" | "sequence">;
|
||||
|
||||
// this (lossily) encodes the two pieces of info (uint64) needed to sign into
|
||||
// one (53-bit) number. Cross your fingers.
|
||||
export function accountToNonce({ account_number: account, sequence }: NonceInfo): Nonce {
|
||||
export function accountToNonce({ account_number: account, sequence }: types.NonceInfo): Nonce {
|
||||
// we allow 23 bits (8 million) for accounts, and 20 bits (1 million) for tx/account
|
||||
// let's fix this soon
|
||||
if (account > maxAcct) {
|
||||
|
||||
3
packages/bcp/types/decode.d.ts
vendored
3
packages/bcp/types/decode.d.ts
vendored
@ -7,7 +7,6 @@ import {
|
||||
FullSignature,
|
||||
Nonce,
|
||||
PubkeyBundle,
|
||||
SendTransaction,
|
||||
SignatureBytes,
|
||||
SignedTransaction,
|
||||
UnsignedTransaction,
|
||||
@ -19,7 +18,7 @@ export declare function decodeSignature(signature: string): SignatureBytes;
|
||||
export declare function decodeFullSignature(signature: types.StdSignature, nonce: number): FullSignature;
|
||||
export declare function coinToDecimal(tokens: TokenInfos, coin: types.Coin): readonly [Decimal, string];
|
||||
export declare function decodeAmount(tokens: TokenInfos, coin: types.Coin): Amount;
|
||||
export declare function parseMsg(msg: types.Msg, chainId: ChainId, tokens: TokenInfos): SendTransaction;
|
||||
export declare function parseMsg(msg: types.Msg, chainId: ChainId, tokens: TokenInfos): UnsignedTransaction;
|
||||
export declare function parseFee(fee: types.StdFee, tokens: TokenInfos): Fee;
|
||||
export declare function parseTx(
|
||||
txValue: types.StdTx,
|
||||
|
||||
3
packages/bcp/types/types.d.ts
vendored
3
packages/bcp/types/types.d.ts
vendored
@ -15,7 +15,6 @@ export interface TokenInfo {
|
||||
readonly fractionalDigits: number;
|
||||
}
|
||||
export declare type TokenInfos = ReadonlyArray<TokenInfo>;
|
||||
export declare type NonceInfo = Pick<types.BaseAccount, "account_number" | "sequence">;
|
||||
export declare function accountToNonce({ account_number: account, sequence }: NonceInfo): Nonce;
|
||||
export declare function accountToNonce({ account_number: account, sequence }: types.NonceInfo): Nonce;
|
||||
export declare function nonceToAccountNumber(nonce: Nonce): number;
|
||||
export declare function nonceToSequence(nonce: Nonce): number;
|
||||
|
||||
@ -38,9 +38,12 @@
|
||||
"pack-web": "yarn build-or-skip && webpack --mode development --config webpack.web.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iov/crypto": "^2.0.0-alpha.7",
|
||||
"@iov/encoding": "^2.0.0-alpha.7",
|
||||
"axios": "^0.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iov/bcp": "^2.0.0-alpha.7",
|
||||
"@iov/keycontrol": "^2.0.0-alpha.7"
|
||||
}
|
||||
}
|
||||
|
||||
59
packages/sdk/src/encoding.spec.ts
Normal file
59
packages/sdk/src/encoding.spec.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { encodeSecp256k1Signature } from "./encoding";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
describe("encoding", () => {
|
||||
describe("encodeSecp256k1Signature", () => {
|
||||
it("encodes a full signature", () => {
|
||||
const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP");
|
||||
const signature = fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
);
|
||||
expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
|
||||
},
|
||||
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
});
|
||||
});
|
||||
|
||||
it("compresses uncompressed public keys", () => {
|
||||
const pubkey = fromBase64(
|
||||
"BE8EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQE7WHpoHoNswYeoFkuYpYSKK4mzFzMV/dB0DVAy4lnNU=",
|
||||
);
|
||||
const signature = fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
);
|
||||
expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
|
||||
},
|
||||
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
});
|
||||
});
|
||||
|
||||
it("removes recovery values from signature data", () => {
|
||||
const pubkey = fromBase64("AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP");
|
||||
const signature = Uint8Array.from([
|
||||
...fromBase64(
|
||||
"1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
),
|
||||
99,
|
||||
]);
|
||||
expect(encodeSecp256k1Signature(pubkey, signature)).toEqual({
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "AtQaCqFnshaZQp6rIkvAPyzThvCvXSDO+9AzbxVErqJP",
|
||||
},
|
||||
signature: "1nUcIH0CLT0/nQ0mBTDrT6kMG20NY/PsH7P2gc4bpYNGLEYjBmdWevXUJouSE/9A/60QG9cYeqyTe5kFDeIPxQ==",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,8 +1,71 @@
|
||||
import { Secp256k1 } from "@iov/crypto";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { StdTx } from "./types";
|
||||
import { Msg, NonceInfo, StdFee, StdSignature, StdTx } from "./types";
|
||||
|
||||
const { toBase64, toUtf8 } = Encoding;
|
||||
|
||||
function sortJson(json: any): any {
|
||||
if (typeof json !== "object" || json === null) {
|
||||
return json;
|
||||
}
|
||||
if (Array.isArray(json)) {
|
||||
return json.map(sortJson);
|
||||
}
|
||||
const sortedKeys = Object.keys(json).sort();
|
||||
const result = sortedKeys.reduce(
|
||||
(accumulator, key) => ({
|
||||
...accumulator,
|
||||
[key]: sortJson(json[key]),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function marshalTx(tx: StdTx): Uint8Array {
|
||||
const json = JSON.stringify(tx);
|
||||
return Encoding.toUtf8(json);
|
||||
}
|
||||
|
||||
interface SignJson {
|
||||
readonly account_number: string;
|
||||
readonly chain_id: string;
|
||||
readonly fee: StdFee;
|
||||
readonly memo: string;
|
||||
readonly msgs: readonly Msg[];
|
||||
readonly sequence: string;
|
||||
}
|
||||
|
||||
export function makeSignBytes(
|
||||
msgs: readonly Msg[],
|
||||
fee: StdFee,
|
||||
chainId: string,
|
||||
memo: string,
|
||||
account: NonceInfo,
|
||||
): Uint8Array {
|
||||
const signJson: SignJson = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
account_number: account.account_number.toString(),
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
chain_id: chainId,
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
msgs: msgs,
|
||||
sequence: account.sequence.toString(),
|
||||
};
|
||||
const signMsg = sortJson(signJson);
|
||||
return toUtf8(JSON.stringify(signMsg));
|
||||
}
|
||||
|
||||
export function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
pub_key: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: toBase64(Secp256k1.compressPubkey(pubkey)),
|
||||
},
|
||||
// Recovery seems to be unused
|
||||
signature: toBase64(Secp256k1.trimRecoveryByte(signature)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as types from "./types";
|
||||
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { marshalTx } from "./encoding";
|
||||
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export { types };
|
||||
|
||||
124
packages/sdk/src/logs.spec.ts
Normal file
124
packages/sdk/src/logs.spec.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { parseAttribute, parseEvent, parseLog, parseLogs } from "./logs";
|
||||
|
||||
describe("logs", () => {
|
||||
describe("parseAttribute", () => {
|
||||
it("works", () => {
|
||||
const attr = parseAttribute({ key: "a", value: "b" });
|
||||
expect(attr).toEqual({ key: "a", value: "b" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseEvent", () => {
|
||||
it("works", () => {
|
||||
const original = {
|
||||
type: "message",
|
||||
attributes: [
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "module",
|
||||
value: "wasm",
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "sender",
|
||||
value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
},
|
||||
{
|
||||
key: "code_id",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
const event = parseEvent(original);
|
||||
expect(event).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseLog", () => {
|
||||
it("works", () => {
|
||||
const original = {
|
||||
msg_index: 0,
|
||||
log: "",
|
||||
events: [
|
||||
{
|
||||
type: "message",
|
||||
attributes: [
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "module",
|
||||
value: "wasm",
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "sender",
|
||||
value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
},
|
||||
{
|
||||
key: "code_id",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
const log = parseLog(original);
|
||||
expect(log).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseLogs", () => {
|
||||
it("works", () => {
|
||||
const original = [
|
||||
{
|
||||
msg_index: 0,
|
||||
log: "",
|
||||
events: [
|
||||
{
|
||||
type: "message",
|
||||
attributes: [
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "module",
|
||||
value: "wasm",
|
||||
},
|
||||
{
|
||||
key: "action",
|
||||
value: "store-code",
|
||||
},
|
||||
{
|
||||
key: "sender",
|
||||
value: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
},
|
||||
{
|
||||
key: "code_id",
|
||||
value: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const logs = parseLogs(original);
|
||||
expect(logs).toEqual(original);
|
||||
});
|
||||
});
|
||||
});
|
||||
59
packages/sdk/src/logs.ts
Normal file
59
packages/sdk/src/logs.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { isNonNullObject } from "@iov/encoding";
|
||||
|
||||
export interface Attribute {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
readonly type: "message";
|
||||
readonly attributes: readonly Attribute[];
|
||||
}
|
||||
|
||||
export interface Log {
|
||||
readonly msg_index: number;
|
||||
readonly log: string;
|
||||
readonly events: readonly Event[];
|
||||
}
|
||||
|
||||
export function parseAttribute(input: unknown): Attribute {
|
||||
if (!isNonNullObject(input)) throw new Error("Attribute must be a non-null object");
|
||||
const { key, value } = input as any;
|
||||
if (typeof key !== "string" || typeof value !== "string") {
|
||||
throw new Error("Attribute is not a key/value pair");
|
||||
}
|
||||
return {
|
||||
key: key,
|
||||
value: value,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseEvent(input: unknown): Event {
|
||||
if (!isNonNullObject(input)) throw new Error("Event must be a non-null object");
|
||||
const { type, attributes } = input as any;
|
||||
if (type !== "message") throw new Error("Event must be of type message");
|
||||
if (!Array.isArray(attributes)) throw new Error("Event's attributes must be an array");
|
||||
return {
|
||||
type: type,
|
||||
attributes: attributes.map(parseAttribute),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLog(input: unknown): Log {
|
||||
if (!isNonNullObject(input)) throw new Error("Log must be a non-null object");
|
||||
const { msg_index, log, events } = input as any;
|
||||
if (typeof msg_index !== "number") throw new Error("Log's msg_index must be a number");
|
||||
if (typeof log !== "string") throw new Error("Log's log must be a string");
|
||||
if (!Array.isArray(events)) throw new Error("Log's events must be an array");
|
||||
return {
|
||||
msg_index: msg_index,
|
||||
log: log,
|
||||
events: events.map(parseEvent),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseLogs(input: unknown): readonly Log[] {
|
||||
if (!Array.isArray(input)) throw new Error("Logs must be an array");
|
||||
return input.map(parseLog);
|
||||
}
|
||||
@ -1,15 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { ChainId, PrehashType, SignableBytes } from "@iov/bcp";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol";
|
||||
|
||||
import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
import { Log, parseLogs } from "./logs";
|
||||
import { RestClient } from "./restclient";
|
||||
import contract from "./testdata/contract.json";
|
||||
import data from "./testdata/cosmoshub.json";
|
||||
import { StdTx } from "./types";
|
||||
import { MsgSend, MsgStoreCode, StdFee, StdTx } from "./types";
|
||||
|
||||
const { fromBase64 } = Encoding;
|
||||
|
||||
const httpUrl = "http://localhost:1317";
|
||||
const defaultNetworkId = "testing";
|
||||
const faucetMnemonic =
|
||||
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone";
|
||||
const faucetPath = HdPaths.cosmos(0);
|
||||
const faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6";
|
||||
const emptyAddress = "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k";
|
||||
|
||||
function pendingWithoutCosmos(): void {
|
||||
if (!process.env.COSMOS_ENABLED) {
|
||||
@ -17,6 +26,11 @@ function pendingWithoutCosmos(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function parseSuccess(rawLog?: string): readonly Log[] {
|
||||
if (!rawLog) throw new Error("Log missing");
|
||||
return parseLogs(JSON.parse(rawLog));
|
||||
}
|
||||
|
||||
describe("RestClient", () => {
|
||||
it("can be constructed", () => {
|
||||
const client = new RestClient(httpUrl);
|
||||
@ -51,4 +65,103 @@ describe("RestClient", () => {
|
||||
expect(await client.encodeTx(tx)).toEqual(fromBase64(data.tx_data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("post", () => {
|
||||
it("can send tokens", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
|
||||
const signer = await wallet.createIdentity("abc" as ChainId, faucetPath);
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const theMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
from_address: faucetAddress,
|
||||
to_address: emptyAddress,
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const client = new RestClient(httpUrl);
|
||||
const account = (await client.authAccounts(faucetAddress)).result.value;
|
||||
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes;
|
||||
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
|
||||
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
|
||||
|
||||
const signedTx: StdTx = {
|
||||
msg: [theMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
|
||||
const postableBytes = marshalTx(signedTx);
|
||||
const result = await client.postTx(postableBytes);
|
||||
// console.log("Raw log:", result.raw_log);
|
||||
expect(result.code).toBeFalsy();
|
||||
});
|
||||
|
||||
it("can upload wasm", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const wallet = Secp256k1HdWallet.fromMnemonic(faucetMnemonic);
|
||||
const signer = await wallet.createIdentity("abc" as ChainId, faucetPath);
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const theMsg: MsgStoreCode = {
|
||||
type: "wasm/store-code",
|
||||
value: {
|
||||
sender: faucetAddress,
|
||||
wasm_byte_code: contract.data,
|
||||
source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm",
|
||||
builder: "cosmwasm-opt:0.6.2",
|
||||
},
|
||||
};
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "89000000",
|
||||
};
|
||||
|
||||
const client = new RestClient(httpUrl);
|
||||
const account = (await client.authAccounts(faucetAddress)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account) as SignableBytes;
|
||||
const rawSignature = await wallet.createTransactionSignature(signer, signBytes, PrehashType.Sha256);
|
||||
const signature = encodeSecp256k1Signature(signer.pubkey.data, rawSignature);
|
||||
|
||||
const signedTx: StdTx = {
|
||||
msg: [theMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
|
||||
const postableBytes = marshalTx(signedTx);
|
||||
const result = await client.postTx(postableBytes);
|
||||
// console.log("Raw log:", result.raw_log);
|
||||
expect(result.code).toBeFalsy();
|
||||
const [firstLog] = parseSuccess(result.raw_log);
|
||||
const codeIdAttr = firstLog.events[0].attributes.find(attr => attr.key === "code_id");
|
||||
expect(codeIdAttr).toEqual({ key: "code_id", value: "1" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
4
packages/sdk/src/testdata/contract.json
vendored
Normal file
4
packages/sdk/src/testdata/contract.json
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -22,13 +22,12 @@ export function isAminoStdTx(txValue: unknown): txValue is StdTx {
|
||||
);
|
||||
}
|
||||
|
||||
export interface Msg {
|
||||
interface MsgTemplate {
|
||||
readonly type: string;
|
||||
// TODO: make better union type
|
||||
readonly value: MsgSend | unknown;
|
||||
readonly value: object;
|
||||
}
|
||||
|
||||
export interface MsgSend {
|
||||
export interface ValueSend {
|
||||
/** Bech32 account address */
|
||||
readonly from_address: string;
|
||||
/** Bech32 account address */
|
||||
@ -36,6 +35,37 @@ export interface MsgSend {
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
}
|
||||
|
||||
export interface MsgSend extends MsgTemplate {
|
||||
readonly type: "cosmos-sdk/MsgSend";
|
||||
readonly value: ValueSend;
|
||||
}
|
||||
|
||||
export interface ValueStoreCode {
|
||||
/** Bech32 account address */
|
||||
readonly sender: string;
|
||||
/** Base64 encoded Wasm */
|
||||
readonly wasm_byte_code: string;
|
||||
/** A valid URI reference to the contract's source code, optional */
|
||||
readonly source?: string;
|
||||
/** A docker tag, optional */
|
||||
readonly builder?: string;
|
||||
}
|
||||
|
||||
export interface MsgStoreCode extends MsgTemplate {
|
||||
readonly type: "wasm/store-code";
|
||||
readonly value: ValueStoreCode;
|
||||
}
|
||||
|
||||
export type Msg = MsgSend | MsgStoreCode | MsgTemplate;
|
||||
|
||||
export function isMsgSend(msg: Msg): msg is MsgSend {
|
||||
return (msg as MsgSend).type === "cosmos-sdk/MsgSend";
|
||||
}
|
||||
|
||||
export function isMsgStoreCode(msg: Msg): msg is MsgStoreCode {
|
||||
return (msg as MsgStoreCode).type === "wasm/store-code";
|
||||
}
|
||||
|
||||
export interface StdFee {
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
readonly gas: string;
|
||||
@ -67,3 +97,6 @@ export interface BaseAccount {
|
||||
readonly account_number: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
|
||||
/** The data we need from BaseAccount to create a nonce */
|
||||
export type NonceInfo = Pick<BaseAccount, "account_number" | "sequence">;
|
||||
|
||||
10
packages/sdk/types/encoding.d.ts
vendored
10
packages/sdk/types/encoding.d.ts
vendored
@ -1,2 +1,10 @@
|
||||
import { StdTx } from "./types";
|
||||
import { Msg, NonceInfo, StdFee, StdSignature, StdTx } from "./types";
|
||||
export declare function marshalTx(tx: StdTx): Uint8Array;
|
||||
export declare function makeSignBytes(
|
||||
msgs: readonly Msg[],
|
||||
fee: StdFee,
|
||||
chainId: string,
|
||||
memo: string,
|
||||
account: NonceInfo,
|
||||
): Uint8Array;
|
||||
export declare function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature;
|
||||
|
||||
2
packages/sdk/types/index.d.ts
vendored
2
packages/sdk/types/index.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
import * as types from "./types";
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { marshalTx } from "./encoding";
|
||||
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export { types };
|
||||
|
||||
17
packages/sdk/types/logs.d.ts
vendored
Normal file
17
packages/sdk/types/logs.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
export interface Attribute {
|
||||
readonly key: string;
|
||||
readonly value: string;
|
||||
}
|
||||
export interface Event {
|
||||
readonly type: "message";
|
||||
readonly attributes: readonly Attribute[];
|
||||
}
|
||||
export interface Log {
|
||||
readonly msg_index: number;
|
||||
readonly log: string;
|
||||
readonly events: readonly Event[];
|
||||
}
|
||||
export declare function parseAttribute(input: unknown): Attribute;
|
||||
export declare function parseEvent(input: unknown): Event;
|
||||
export declare function parseLog(input: unknown): Log;
|
||||
export declare function parseLogs(input: unknown): readonly Log[];
|
||||
30
packages/sdk/types/types.d.ts
vendored
30
packages/sdk/types/types.d.ts
vendored
@ -12,17 +12,38 @@ export declare type AminoTx = Tx & {
|
||||
readonly value: StdTx;
|
||||
};
|
||||
export declare function isAminoStdTx(txValue: unknown): txValue is StdTx;
|
||||
export interface Msg {
|
||||
interface MsgTemplate {
|
||||
readonly type: string;
|
||||
readonly value: MsgSend | unknown;
|
||||
readonly value: object;
|
||||
}
|
||||
export interface MsgSend {
|
||||
export interface ValueSend {
|
||||
/** Bech32 account address */
|
||||
readonly from_address: string;
|
||||
/** Bech32 account address */
|
||||
readonly to_address: string;
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
}
|
||||
export interface MsgSend extends MsgTemplate {
|
||||
readonly type: "cosmos-sdk/MsgSend";
|
||||
readonly value: ValueSend;
|
||||
}
|
||||
export interface ValueStoreCode {
|
||||
/** Bech32 account address */
|
||||
readonly sender: string;
|
||||
/** Base64 encoded Wasm */
|
||||
readonly wasm_byte_code: string;
|
||||
/** A valid URI reference to the contract's source code, optional */
|
||||
readonly source?: string;
|
||||
/** A docker tag, optional */
|
||||
readonly builder?: string;
|
||||
}
|
||||
export interface MsgStoreCode extends MsgTemplate {
|
||||
readonly type: "wasm/store-code";
|
||||
readonly value: ValueStoreCode;
|
||||
}
|
||||
export declare type Msg = MsgSend | MsgStoreCode | MsgTemplate;
|
||||
export declare function isMsgSend(msg: Msg): msg is MsgSend;
|
||||
export declare function isMsgStoreCode(msg: Msg): msg is MsgStoreCode;
|
||||
export interface StdFee {
|
||||
readonly amount: ReadonlyArray<Coin>;
|
||||
readonly gas: string;
|
||||
@ -48,3 +69,6 @@ export interface BaseAccount {
|
||||
readonly account_number: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
/** The data we need from BaseAccount to create a nonce */
|
||||
export declare type NonceInfo = Pick<BaseAccount, "account_number" | "sequence">;
|
||||
export {};
|
||||
|
||||
41
scripts/cosm/manual_start.sh
Executable file
41
scripts/cosm/manual_start.sh
Executable file
@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -o errexit -o nounset -o pipefail
|
||||
command -v shellcheck > /dev/null && shellcheck "$0"
|
||||
|
||||
## This is like start.sh but using local binaries, not docker images
|
||||
SCRIPT_DIR="$(realpath "$(dirname "$0")")"
|
||||
|
||||
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gaia.XXXXXXXXX")
|
||||
chmod 777 "$TMP_DIR"
|
||||
echo "Using temporary dir $TMP_DIR"
|
||||
WASMD_LOGFILE="$TMP_DIR/wasmd.log"
|
||||
REST_SERVER_LOGFILE="$TMP_DIR/rest-server.log"
|
||||
|
||||
# move the template into our temporary home
|
||||
cp -r "$SCRIPT_DIR"/template/.wasm* "$TMP_DIR"
|
||||
|
||||
wasmd start \
|
||||
--home "$TMP_DIR/.wasmd" \
|
||||
--trace \
|
||||
--rpc.laddr tcp://0.0.0.0:26657 \
|
||||
> "$WASMD_LOGFILE" &
|
||||
|
||||
echo "wasmd running and logging into $WASMD_LOGFILE"
|
||||
|
||||
sleep 10
|
||||
cat "$WASMD_LOGFILE"
|
||||
|
||||
wasmcli rest-server \
|
||||
--home "$TMP_DIR/.wasmcli" \
|
||||
--node tcp://localhost:26657 \
|
||||
--trust-node \
|
||||
--laddr tcp://0.0.0.0:1317 \
|
||||
> "$REST_SERVER_LOGFILE" &
|
||||
|
||||
echo "rest server running on http://localhost:1317 and logging into $REST_SERVER_LOGFILE"
|
||||
|
||||
# Debug rest server start
|
||||
sleep 3
|
||||
cat "$REST_SERVER_LOGFILE"
|
||||
|
||||
tail -f "$WASMD_LOGFILE"
|
||||
Loading…
Reference in New Issue
Block a user