Merge pull request #74 from confio/cosmwasmclient
Add CosmWasmClient to SDK
This commit is contained in:
commit
1e4b1f512a
@ -4,7 +4,6 @@ import {
|
||||
isValidAddress,
|
||||
makeSignBytes,
|
||||
marshalTx,
|
||||
types,
|
||||
unmarshalTx,
|
||||
} from "@cosmwasm/sdk";
|
||||
import {
|
||||
@ -46,16 +45,13 @@ export class CosmWasmCodec implements TxCodec {
|
||||
public bytesToSign(unsigned: UnsignedTransaction, nonce: Nonce): SigningJob {
|
||||
const built = buildUnsignedTx(unsigned, this.bankTokens, this.erc20Tokens);
|
||||
|
||||
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,
|
||||
nonceToAccountNumber(nonce),
|
||||
nonceToSequence(nonce),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { CosmosAddressBech32Prefix, RestClient, TxsResponse, types, unmarshalTx } from "@cosmwasm/sdk";
|
||||
import {
|
||||
CosmosAddressBech32Prefix,
|
||||
CosmWasmClient,
|
||||
RestClient,
|
||||
TxsResponse,
|
||||
types,
|
||||
unmarshalTx,
|
||||
} from "@cosmwasm/sdk";
|
||||
import {
|
||||
Account,
|
||||
AccountQuery,
|
||||
@ -86,16 +93,19 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
tokens: TokenConfiguration,
|
||||
): Promise<CosmWasmConnection> {
|
||||
const restClient = new RestClient(url);
|
||||
const chainData = await this.initialize(restClient);
|
||||
return new CosmWasmConnection(restClient, chainData, addressPrefix, tokens);
|
||||
const cosmWasmClient = CosmWasmClient.makeReadOnly(url);
|
||||
const chainData = await this.initialize(cosmWasmClient);
|
||||
return new CosmWasmConnection(restClient, cosmWasmClient, chainData, addressPrefix, tokens);
|
||||
}
|
||||
|
||||
private static async initialize(restClient: RestClient): Promise<ChainData> {
|
||||
const { node_info } = await restClient.nodeInfo();
|
||||
return { chainId: Caip5.encode(node_info.network) };
|
||||
private static async initialize(cosmWasmClient: CosmWasmClient): Promise<ChainData> {
|
||||
const rawChainId = await cosmWasmClient.chainId();
|
||||
return { chainId: Caip5.encode(rawChainId) };
|
||||
}
|
||||
|
||||
/** @deprecated everything we use from RestClient should be available in CosmWasmClient */
|
||||
private readonly restClient: RestClient;
|
||||
private readonly cosmWasmClient: CosmWasmClient;
|
||||
private readonly chainData: ChainData;
|
||||
private readonly addressPrefix: CosmosAddressBech32Prefix;
|
||||
private readonly bankTokens: readonly BankToken[];
|
||||
@ -107,11 +117,14 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
|
||||
private constructor(
|
||||
restClient: RestClient,
|
||||
cosmWasmClient: CosmWasmClient,
|
||||
chainData: ChainData,
|
||||
addressPrefix: CosmosAddressBech32Prefix,
|
||||
tokens: TokenConfiguration,
|
||||
) {
|
||||
// tslint:disable-next-line: deprecation
|
||||
this.restClient = restClient;
|
||||
this.cosmWasmClient = cosmWasmClient;
|
||||
this.chainData = chainData;
|
||||
this.addressPrefix = addressPrefix;
|
||||
this.bankTokens = tokens.bankTokens;
|
||||
@ -136,6 +149,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
}
|
||||
|
||||
public async height(): Promise<number> {
|
||||
// tslint:disable-next-line: deprecation
|
||||
const { block } = await this.restClient.blocksLatest();
|
||||
return block.header.height;
|
||||
}
|
||||
@ -150,6 +164,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
|
||||
public async identifier(signed: PostableBytes): Promise<TransactionId> {
|
||||
const tx = unmarshalTx(signed);
|
||||
// tslint:disable-next-line: deprecation
|
||||
const bytes = await this.restClient.encodeTx(tx);
|
||||
const hash = new Sha256(bytes).digest();
|
||||
return toHex(hash).toUpperCase() as TransactionId;
|
||||
@ -157,6 +172,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
|
||||
public async getAccount(query: AccountQuery): Promise<Account | undefined> {
|
||||
const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address;
|
||||
// tslint:disable-next-line: deprecation
|
||||
const { result } = await this.restClient.authAccounts(address);
|
||||
const bankAccount = result.value;
|
||||
const hasBankAccount = !!bankAccount.address;
|
||||
@ -168,6 +184,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
this.erc20Tokens.map(
|
||||
async (erc20): Promise<Amount> => {
|
||||
const queryMsg = { balance: { address: address } };
|
||||
// tslint:disable-next-line: deprecation
|
||||
const smart = await this.restClient.queryContractSmart(erc20.contractAddress, queryMsg);
|
||||
const response = JSON.parse(fromAscii(smart));
|
||||
const normalizedBalance = new BN(response.balance).toString();
|
||||
@ -203,9 +220,8 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
|
||||
public async getNonce(query: AddressQuery | PubkeyQuery): Promise<Nonce> {
|
||||
const address = isPubkeyQuery(query) ? pubkeyToAddress(query.pubkey, this.addressPrefix) : query.address;
|
||||
const { result } = await this.restClient.authAccounts(address);
|
||||
const account = result.value;
|
||||
return accountToNonce(account);
|
||||
const { accountNumber, sequence } = await this.cosmWasmClient.getNonce(address);
|
||||
return accountToNonce(accountNumber, sequence);
|
||||
}
|
||||
|
||||
public async getNonces(query: AddressQuery | PubkeyQuery, count: number): Promise<readonly Nonce[]> {
|
||||
@ -219,6 +235,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
}
|
||||
|
||||
public async getBlockHeader(height: number): Promise<BlockHeader> {
|
||||
// tslint:disable-next-line: deprecation
|
||||
const { block_meta } = await this.restClient.blocks(height);
|
||||
return {
|
||||
id: block_meta.block_id.hash as BlockId,
|
||||
@ -236,6 +253,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
id: TransactionId,
|
||||
): Promise<ConfirmedAndSignedTransaction<UnsignedTransaction> | FailedTransaction> {
|
||||
try {
|
||||
// tslint:disable-next-line: deprecation
|
||||
const response = await this.restClient.txsById(id);
|
||||
const chainId = this.chainId();
|
||||
return this.parseAndPopulateTxResponse(response, chainId);
|
||||
@ -248,11 +266,8 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
}
|
||||
|
||||
public async postTx(tx: PostableBytes): Promise<PostTxResponse> {
|
||||
const { code, txhash, raw_log } = await this.restClient.postTx(tx);
|
||||
if (code) {
|
||||
throw new Error(raw_log);
|
||||
}
|
||||
const transactionId = txhash as TransactionId;
|
||||
const { transactionHash, rawLog } = await this.cosmWasmClient.postTx(tx);
|
||||
const transactionId = transactionHash as TransactionId;
|
||||
const firstEvent: BlockInfo = { state: TransactionState.Pending };
|
||||
let blockInfoInterval: NodeJS.Timeout;
|
||||
let lastEventSent: BlockInfo;
|
||||
@ -285,7 +300,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
return {
|
||||
blockInfo: new ValueAndUpdates<BlockInfo>(producer),
|
||||
transactionId: transactionId,
|
||||
log: raw_log,
|
||||
log: rawLog,
|
||||
};
|
||||
}
|
||||
|
||||
@ -295,6 +310,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
const queryString = buildQueryString(query);
|
||||
const chainId = this.chainId();
|
||||
// TODO: we need pagination support
|
||||
// tslint:disable-next-line: deprecation
|
||||
const response = await this.restClient.txs(queryString + "&limit=50");
|
||||
const { txs } = response;
|
||||
return Promise.all(txs.map(tx => this.parseAndPopulateTxResponse(tx, chainId)));
|
||||
@ -354,6 +370,7 @@ export class CosmWasmConnection implements BlockchainConnection {
|
||||
throw new Error(`Got unsupported type of message: ${firstMsg.type}`);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: deprecation
|
||||
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
|
||||
|
||||
@ -109,8 +109,8 @@ export function parseTx(
|
||||
nonce: Nonce,
|
||||
tokens: BankTokens,
|
||||
): SignedTransaction {
|
||||
if (!types.isAminoStdTx(txValue)) {
|
||||
throw new Error("Only Amino StdTx is supported");
|
||||
if (!types.isStdTx(txValue)) {
|
||||
throw new Error("Only StdTx is supported");
|
||||
}
|
||||
if (txValue.msg.length !== 1) {
|
||||
throw new Error("Only single-message transactions currently supported");
|
||||
|
||||
@ -81,7 +81,7 @@ export function buildUnsignedTx(
|
||||
tx: UnsignedTransaction,
|
||||
bankTokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[] = [],
|
||||
): types.AminoTx {
|
||||
): types.CosmosSdkTx {
|
||||
if (!isSendTransaction(tx)) {
|
||||
throw new Error("Received transaction of unsupported kind");
|
||||
}
|
||||
@ -152,7 +152,7 @@ export function buildSignedTx(
|
||||
tx: SignedTransaction,
|
||||
bankTokens: BankTokens,
|
||||
erc20Tokens: readonly Erc20Token[] = [],
|
||||
): types.AminoTx {
|
||||
): types.CosmosSdkTx {
|
||||
const built = buildUnsignedTx(tx.transaction, bankTokens, erc20Tokens);
|
||||
return {
|
||||
...built,
|
||||
|
||||
@ -3,26 +3,13 @@ import { accountToNonce, nonceToAccountNumber, nonceToSequence } from "./types";
|
||||
|
||||
describe("nonceEncoding", () => {
|
||||
it("works for input in range", () => {
|
||||
const nonce = accountToNonce({
|
||||
account_number: 1234,
|
||||
sequence: 7890,
|
||||
});
|
||||
const nonce = accountToNonce(1234, 7890);
|
||||
expect(nonceToAccountNumber(nonce)).toEqual(1234);
|
||||
expect(nonceToSequence(nonce)).toEqual(7890);
|
||||
});
|
||||
|
||||
it("errors on input too large", () => {
|
||||
expect(() =>
|
||||
accountToNonce({
|
||||
account_number: 1234567890,
|
||||
sequence: 7890,
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
accountToNonce({
|
||||
account_number: 178,
|
||||
sequence: 97320247923,
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() => accountToNonce(1234567890, 7890)).toThrow();
|
||||
expect(() => accountToNonce(178, 97320247923)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { types } from "@cosmwasm/sdk";
|
||||
import { Nonce } from "@iov/bcp";
|
||||
|
||||
export interface BankToken {
|
||||
@ -40,17 +39,17 @@ const maxSeq = 1 << 20;
|
||||
|
||||
// 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 }: types.NonceInfo): Nonce {
|
||||
export function accountToNonce(accountNumber: number, sequence: number): 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) {
|
||||
if (accountNumber > maxAcct) {
|
||||
throw new Error("Account number is greater than 2^23, must update Nonce handler");
|
||||
}
|
||||
if (sequence > maxSeq) {
|
||||
throw new Error("Sequence is greater than 2^20, must update Nonce handler");
|
||||
}
|
||||
|
||||
const val = account * maxSeq + sequence;
|
||||
const val = accountNumber * maxSeq + sequence;
|
||||
return val as Nonce;
|
||||
}
|
||||
|
||||
|
||||
2
packages/bcp/types/cosmwasmconnection.d.ts
vendored
2
packages/bcp/types/cosmwasmconnection.d.ts
vendored
@ -43,7 +43,9 @@ export declare class CosmWasmConnection implements BlockchainConnection {
|
||||
tokens: TokenConfiguration,
|
||||
): Promise<CosmWasmConnection>;
|
||||
private static initialize;
|
||||
/** @deprecated everything we use from RestClient should be available in CosmWasmClient */
|
||||
private readonly restClient;
|
||||
private readonly cosmWasmClient;
|
||||
private readonly chainData;
|
||||
private readonly addressPrefix;
|
||||
private readonly bankTokens;
|
||||
|
||||
4
packages/bcp/types/encode.d.ts
vendored
4
packages/bcp/types/encode.d.ts
vendored
@ -10,9 +10,9 @@ export declare function buildUnsignedTx(
|
||||
tx: UnsignedTransaction,
|
||||
bankTokens: BankTokens,
|
||||
erc20Tokens?: readonly Erc20Token[],
|
||||
): types.AminoTx;
|
||||
): types.CosmosSdkTx;
|
||||
export declare function buildSignedTx(
|
||||
tx: SignedTransaction,
|
||||
bankTokens: BankTokens,
|
||||
erc20Tokens?: readonly Erc20Token[],
|
||||
): types.AminoTx;
|
||||
): types.CosmosSdkTx;
|
||||
|
||||
3
packages/bcp/types/types.d.ts
vendored
3
packages/bcp/types/types.d.ts
vendored
@ -1,4 +1,3 @@
|
||||
import { types } from "@cosmwasm/sdk";
|
||||
import { Nonce } from "@iov/bcp";
|
||||
export interface BankToken {
|
||||
readonly denom: string;
|
||||
@ -29,6 +28,6 @@ export interface Erc20Token {
|
||||
*/
|
||||
readonly fractionalDigits: number;
|
||||
}
|
||||
export declare function accountToNonce({ account_number: account, sequence }: types.NonceInfo): Nonce;
|
||||
export declare function accountToNonce(accountNumber: number, sequence: number): Nonce;
|
||||
export declare function nonceToAccountNumber(nonce: Nonce): number;
|
||||
export declare function nonceToSequence(nonce: Nonce): number;
|
||||
|
||||
@ -45,7 +45,7 @@ $ cosmwasm-cli
|
||||
|
||||
```ts
|
||||
// Get account information
|
||||
const account = (await client.authAccounts(faucetAddress)).result.value;
|
||||
const { account_number, sequence } = (await client.authAccounts(faucetAddress)).result.value;
|
||||
|
||||
// Craft a send transaction
|
||||
const emptyAddress = Bech32.encode("cosmos", Random.getBytes(20));
|
||||
@ -64,7 +64,7 @@ const sendTokensMsg: types.MsgSend = {
|
||||
},
|
||||
};
|
||||
|
||||
const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account);
|
||||
const signBytes = makeSignBytes([sendTokensMsg], defaultFee, defaultNetworkId, memo, account_number, sequence);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx: types.StdTx = {
|
||||
msg: [sendTokensMsg],
|
||||
|
||||
223
packages/sdk/src/cosmwasmclient.spec.ts
Normal file
223
packages/sdk/src/cosmwasmclient.spec.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { CosmWasmClient } from "./cosmwasmclient";
|
||||
import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
import { findAttribute } from "./logs";
|
||||
import { Secp256k1Pen } from "./pen";
|
||||
import { RestClient } from "./restclient";
|
||||
import { getRandomizedHackatom, makeRandomAddress } from "./testutils.spec";
|
||||
import { Coin, MsgSend, StdFee } from "./types";
|
||||
|
||||
const httpUrl = "http://localhost:1317";
|
||||
|
||||
function cosmosEnabled(): boolean {
|
||||
return !!process.env.COSMOS_ENABLED;
|
||||
}
|
||||
|
||||
function pendingWithoutCosmos(): void {
|
||||
if (!cosmosEnabled()) {
|
||||
return pending("Set COSMOS_ENABLED to enable Cosmos node-based tests");
|
||||
}
|
||||
}
|
||||
|
||||
const faucet = {
|
||||
mnemonic:
|
||||
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone",
|
||||
pubkey: {
|
||||
type: "tendermint/PubKeySecp256k1",
|
||||
value: "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ",
|
||||
},
|
||||
address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
};
|
||||
|
||||
const unusedAccount = {
|
||||
address: "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u",
|
||||
};
|
||||
|
||||
describe("CosmWasmClient", () => {
|
||||
describe("makeReadOnly", () => {
|
||||
it("can be constructed", () => {
|
||||
const client = CosmWasmClient.makeReadOnly(httpUrl);
|
||||
expect(client).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("chainId", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const client = CosmWasmClient.makeReadOnly(httpUrl);
|
||||
expect(await client.chainId()).toEqual("testing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getNonce", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const client = CosmWasmClient.makeReadOnly(httpUrl);
|
||||
expect(await client.getNonce(unusedAccount.address)).toEqual({
|
||||
accountNumber: 5,
|
||||
sequence: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("postTx", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = CosmWasmClient.makeReadOnly(httpUrl);
|
||||
|
||||
const memo = "My first contract on chain";
|
||||
const sendMsg: MsgSend = {
|
||||
type: "cosmos-sdk/MsgSend",
|
||||
value: {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
from_address: faucet.address,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
to_address: makeRandomAddress(),
|
||||
amount: [
|
||||
{
|
||||
denom: "ucosm",
|
||||
amount: "1234567",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const fee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "890000",
|
||||
};
|
||||
|
||||
const chainId = await client.chainId();
|
||||
const { accountNumber, sequence } = await client.getNonce(faucet.address);
|
||||
const signBytes = makeSignBytes([sendMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = {
|
||||
msg: [sendMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
const { logs, transactionHash } = await client.postTx(marshalTx(signedTx));
|
||||
const amountAttr = findAttribute(logs, "transfer", "amount");
|
||||
expect(amountAttr.value).toEqual("1234567ucosm");
|
||||
expect(transactionHash).toMatch(/^[0-9A-F]{64}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("upload", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => {
|
||||
return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
});
|
||||
const codeId = await client.upload(getRandomizedHackatom());
|
||||
expect(codeId).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("instantiate", () => {
|
||||
it("works with transfer amount", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => {
|
||||
return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
});
|
||||
const codeId = await client.upload(getRandomizedHackatom());
|
||||
|
||||
const transferAmount: readonly Coin[] = [
|
||||
{
|
||||
amount: "1234",
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
amount: "321",
|
||||
denom: "ustake",
|
||||
},
|
||||
];
|
||||
const beneficiaryAddress = makeRandomAddress();
|
||||
const contractAddress = await client.instantiate(
|
||||
codeId,
|
||||
{
|
||||
verifier: faucet.address,
|
||||
beneficiary: beneficiaryAddress,
|
||||
},
|
||||
"Let's see",
|
||||
transferAmount,
|
||||
);
|
||||
|
||||
const rest = new RestClient(httpUrl);
|
||||
const balance = (await rest.authAccounts(contractAddress)).result.value.coins;
|
||||
expect(balance).toEqual(transferAmount);
|
||||
});
|
||||
|
||||
it("can instantiate one code multiple times", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => {
|
||||
return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
});
|
||||
const codeId = await client.upload(getRandomizedHackatom());
|
||||
|
||||
const contractAddress1 = await client.instantiate(codeId, {
|
||||
verifier: faucet.address,
|
||||
beneficiary: makeRandomAddress(),
|
||||
});
|
||||
const contractAddress2 = await client.instantiate(codeId, {
|
||||
verifier: faucet.address,
|
||||
beneficiary: makeRandomAddress(),
|
||||
});
|
||||
expect(contractAddress1).not.toEqual(contractAddress2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("execute", () => {
|
||||
it("works", async () => {
|
||||
pendingWithoutCosmos();
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => {
|
||||
return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
});
|
||||
const codeId = await client.upload(getRandomizedHackatom());
|
||||
|
||||
// instantiate
|
||||
const transferAmount: readonly Coin[] = [
|
||||
{
|
||||
amount: "233444",
|
||||
denom: "ucosm",
|
||||
},
|
||||
{
|
||||
amount: "5454",
|
||||
denom: "ustake",
|
||||
},
|
||||
];
|
||||
const beneficiaryAddress = makeRandomAddress();
|
||||
const contractAddress = await client.instantiate(
|
||||
codeId,
|
||||
{
|
||||
verifier: faucet.address,
|
||||
beneficiary: beneficiaryAddress,
|
||||
},
|
||||
undefined,
|
||||
transferAmount,
|
||||
);
|
||||
|
||||
// execute
|
||||
const result = await client.execute(contractAddress, {}, undefined);
|
||||
const [firstLog] = result.logs;
|
||||
expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`);
|
||||
|
||||
// Verify token transfer from contract to beneficiary
|
||||
const rest = new RestClient(httpUrl);
|
||||
const beneficiaryBalance = (await rest.authAccounts(beneficiaryAddress)).result.value.coins;
|
||||
expect(beneficiaryBalance).toEqual(transferAmount);
|
||||
const contractBalance = (await rest.authAccounts(contractAddress)).result.value.coins;
|
||||
expect(contractBalance).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
238
packages/sdk/src/cosmwasmclient.ts
Normal file
238
packages/sdk/src/cosmwasmclient.ts
Normal file
@ -0,0 +1,238 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { makeSignBytes, marshalTx } from "./encoding";
|
||||
import { findAttribute, Log, parseLogs } from "./logs";
|
||||
import { RestClient } from "./restclient";
|
||||
import {
|
||||
Coin,
|
||||
MsgExecuteContract,
|
||||
MsgInstantiateContract,
|
||||
MsgStoreCode,
|
||||
StdFee,
|
||||
StdSignature,
|
||||
} from "./types";
|
||||
|
||||
const defaultUploadFee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "1000000", // one million
|
||||
};
|
||||
|
||||
const defaultInitFee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "500000", // 500k
|
||||
};
|
||||
|
||||
const defaultExecFee: StdFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "200000", // 200k
|
||||
};
|
||||
|
||||
export interface SigningCallback {
|
||||
(signBytes: Uint8Array): Promise<StdSignature>;
|
||||
}
|
||||
|
||||
interface SigningData {
|
||||
readonly senderAddress: string;
|
||||
readonly signCallback: SigningCallback;
|
||||
}
|
||||
|
||||
export interface GetNonceResult {
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
|
||||
export interface PostTxResult {
|
||||
readonly logs: readonly Log[];
|
||||
readonly rawLog: string;
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-exmpty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
|
||||
export interface ExecuteResult {
|
||||
readonly logs: readonly Log[];
|
||||
}
|
||||
|
||||
export class CosmWasmClient {
|
||||
public static makeReadOnly(url: string): CosmWasmClient {
|
||||
return new CosmWasmClient(url);
|
||||
}
|
||||
|
||||
public static makeWritable(
|
||||
url: string,
|
||||
senderAddress: string,
|
||||
signCallback: SigningCallback,
|
||||
): CosmWasmClient {
|
||||
return new CosmWasmClient(url, {
|
||||
senderAddress: senderAddress,
|
||||
signCallback: signCallback,
|
||||
});
|
||||
}
|
||||
|
||||
private readonly restClient: RestClient;
|
||||
private readonly signingData: SigningData | undefined;
|
||||
|
||||
private get senderAddress(): string {
|
||||
if (!this.signingData) throw new Error("Signing data not set in this client");
|
||||
return this.signingData.senderAddress;
|
||||
}
|
||||
|
||||
private get signCallback(): SigningCallback {
|
||||
if (!this.signingData) throw new Error("Signing data not set in this client");
|
||||
return this.signingData.signCallback;
|
||||
}
|
||||
|
||||
private constructor(url: string, signingData?: SigningData) {
|
||||
this.restClient = new RestClient(url);
|
||||
this.signingData = signingData;
|
||||
}
|
||||
|
||||
public async chainId(): Promise<string> {
|
||||
const response = await this.restClient.nodeInfo();
|
||||
return response.node_info.network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns account number and sequence.
|
||||
*
|
||||
* @param address returns data for this address. When unset, the client's sender adddress is used.
|
||||
*/
|
||||
public async getNonce(address?: string): Promise<GetNonceResult> {
|
||||
const account = (await this.restClient.authAccounts(address || this.senderAddress)).result.value;
|
||||
return {
|
||||
accountNumber: account.account_number,
|
||||
sequence: account.sequence,
|
||||
};
|
||||
}
|
||||
|
||||
public async postTx(tx: Uint8Array): Promise<PostTxResult> {
|
||||
const result = await this.restClient.postTx(tx);
|
||||
if (result.code) {
|
||||
throw new Error(`Error when posting tx. Code: ${result.code}; Raw log: ${result.raw_log}`);
|
||||
}
|
||||
|
||||
if (!result.txhash.match(/^([0-9A-F][0-9A-F])+$/)) {
|
||||
throw new Error("Received ill-formatted txhash. Must be non-empty upper-case hex");
|
||||
}
|
||||
|
||||
return {
|
||||
logs: parseLogs(result.logs) || [],
|
||||
rawLog: result.raw_log || "",
|
||||
transactionHash: result.txhash,
|
||||
};
|
||||
}
|
||||
|
||||
/** Uploads code and returns a code ID */
|
||||
public async upload(wasmCode: Uint8Array, memo = ""): Promise<number> {
|
||||
const storeCodeMsg: MsgStoreCode = {
|
||||
type: "wasm/store-code",
|
||||
value: {
|
||||
sender: this.senderAddress,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
wasm_byte_code: Encoding.toBase64(wasmCode),
|
||||
source: "",
|
||||
builder: "",
|
||||
},
|
||||
};
|
||||
const fee = defaultUploadFee;
|
||||
const { accountNumber, sequence } = await this.getNonce();
|
||||
const chainId = await this.chainId();
|
||||
const signBytes = makeSignBytes([storeCodeMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = await this.signCallback(signBytes);
|
||||
const signedTx = {
|
||||
msg: [storeCodeMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
|
||||
const result = await this.postTx(marshalTx(signedTx));
|
||||
const codeIdAttr = findAttribute(result.logs, "message", "code_id");
|
||||
const codeId = Number.parseInt(codeIdAttr.value, 10);
|
||||
return codeId;
|
||||
}
|
||||
|
||||
public async instantiate(
|
||||
codeId: number,
|
||||
initMsg: object,
|
||||
memo = "",
|
||||
transferAmount?: readonly Coin[],
|
||||
): Promise<string> {
|
||||
const instantiateMsg: MsgInstantiateContract = {
|
||||
type: "wasm/instantiate",
|
||||
value: {
|
||||
sender: this.senderAddress,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
code_id: codeId.toString(),
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
init_msg: initMsg,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
init_funds: transferAmount || [],
|
||||
},
|
||||
};
|
||||
const fee = defaultInitFee;
|
||||
const { accountNumber, sequence } = await this.getNonce();
|
||||
const chainId = await this.chainId();
|
||||
const signBytes = makeSignBytes([instantiateMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
|
||||
const signature = await this.signCallback(signBytes);
|
||||
const signedTx = {
|
||||
msg: [instantiateMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
|
||||
const result = await this.postTx(marshalTx(signedTx));
|
||||
const contractAddressAttr = findAttribute(result.logs, "message", "contract_address");
|
||||
return contractAddressAttr.value;
|
||||
}
|
||||
|
||||
public async execute(
|
||||
contractAddress: string,
|
||||
handleMsg: object,
|
||||
memo = "",
|
||||
transferAmount?: readonly Coin[],
|
||||
): Promise<ExecuteResult> {
|
||||
const executeMsg: MsgExecuteContract = {
|
||||
type: "wasm/execute",
|
||||
value: {
|
||||
sender: this.senderAddress,
|
||||
contract: contractAddress,
|
||||
msg: handleMsg,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
sent_funds: transferAmount || [],
|
||||
},
|
||||
};
|
||||
const fee = defaultExecFee;
|
||||
const { accountNumber, sequence } = await this.getNonce();
|
||||
const chainId = await this.chainId();
|
||||
const signBytes = makeSignBytes([executeMsg], fee, chainId, memo, accountNumber, sequence);
|
||||
const signature = await this.signCallback(signBytes);
|
||||
const signedTx = {
|
||||
msg: [executeMsg],
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
|
||||
const result = await this.postTx(marshalTx(signedTx));
|
||||
return {
|
||||
logs: result.logs,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { isAminoStdTx, StdTx } from "./types";
|
||||
import { isStdTx, StdTx } from "./types";
|
||||
|
||||
export function unmarshalTx(data: Uint8Array): StdTx {
|
||||
const decoded = JSON.parse(Encoding.fromUtf8(data));
|
||||
if (!isAminoStdTx(decoded)) {
|
||||
if (!isStdTx(decoded)) {
|
||||
throw new Error("Must be json encoded StdTx");
|
||||
}
|
||||
return decoded;
|
||||
|
||||
@ -2,7 +2,7 @@ import { Secp256k1 } from "@iov/crypto";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
import { encodeSecp256k1Pubkey } from "./pubkey";
|
||||
import { Msg, NonceInfo, StdFee, StdSignature, StdTx } from "./types";
|
||||
import { Msg, StdFee, StdSignature, StdTx } from "./types";
|
||||
|
||||
const { toBase64, toUtf8 } = Encoding;
|
||||
|
||||
@ -43,17 +43,18 @@ export function makeSignBytes(
|
||||
fee: StdFee,
|
||||
chainId: string,
|
||||
memo: string,
|
||||
account: NonceInfo,
|
||||
accountNumber: number,
|
||||
sequence: number,
|
||||
): Uint8Array {
|
||||
const signJson: SignJson = {
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
account_number: account.account_number.toString(),
|
||||
account_number: accountNumber.toString(),
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
chain_id: chainId,
|
||||
fee: fee,
|
||||
memo: memo,
|
||||
msgs: msgs,
|
||||
sequence: account.sequence.toString(),
|
||||
sequence: sequence.toString(),
|
||||
};
|
||||
const signMsg = sortJson(signJson);
|
||||
return toUtf8(JSON.stringify(signMsg));
|
||||
|
||||
@ -6,6 +6,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export { CosmWasmClient, ExecuteResult, GetNonceResult, PostTxResult } from "./cosmwasmclient";
|
||||
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
|
||||
export {
|
||||
CosmosPubkeyBech32Prefix,
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { Encoding } from "@iov/encoding";
|
||||
|
||||
const { fromHex } = Encoding;
|
||||
|
||||
export function leb128Encode(uint: number): Uint8Array {
|
||||
if (uint < 0) throw new Error("Only non-negative values supported");
|
||||
if (uint > 0x7fffffff) throw new Error("Only values in signed int32 range allowed");
|
||||
const out = new Array<number>();
|
||||
let value = uint;
|
||||
do {
|
||||
// tslint:disable: no-bitwise
|
||||
let byte = value & 0b01111111;
|
||||
value >>= 7;
|
||||
|
||||
// more bytes to come: set high order bit of byte
|
||||
if (value !== 0) byte ^= 0b10000000;
|
||||
|
||||
out.push(byte);
|
||||
// tslint:enable: no-bitwise
|
||||
} while (value !== 0);
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
describe("leb128", () => {
|
||||
describe("leb128Encode", () => {
|
||||
it("works for single byte values", () => {
|
||||
// Values in 7 bit range are encoded as one byte
|
||||
expect(leb128Encode(0)).toEqual(fromHex("00"));
|
||||
expect(leb128Encode(20)).toEqual(fromHex("14"));
|
||||
expect(leb128Encode(127)).toEqual(fromHex("7f"));
|
||||
});
|
||||
|
||||
it("works for multi byte values", () => {
|
||||
// from external souce (wasm-objdump)
|
||||
expect(leb128Encode(145)).toEqual(fromHex("9101"));
|
||||
expect(leb128Encode(1539)).toEqual(fromHex("830c"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,16 +1,15 @@
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
import { Random, Sha256 } from "@iov/crypto";
|
||||
import { Bech32, Encoding } from "@iov/encoding";
|
||||
import { Sha256 } from "@iov/crypto";
|
||||
import { Encoding } from "@iov/encoding";
|
||||
import { assert } from "@iov/utils";
|
||||
|
||||
import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
import { leb128Encode } from "./leb128.spec";
|
||||
import { findAttribute, parseLogs } from "./logs";
|
||||
import { Pen, Secp256k1Pen } from "./pen";
|
||||
import { encodeBech32Pubkey } from "./pubkey";
|
||||
import { PostTxsResponse, RestClient } from "./restclient";
|
||||
import contract from "./testdata/contract.json";
|
||||
import cosmoshub from "./testdata/cosmoshub.json";
|
||||
import { getRandomizedHackatom, makeRandomAddress } from "./testutils.spec";
|
||||
import {
|
||||
Coin,
|
||||
Msg,
|
||||
@ -60,36 +59,6 @@ function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature:
|
||||
};
|
||||
}
|
||||
|
||||
function getRandomizedContract(): Uint8Array {
|
||||
const data = fromBase64(contract.data);
|
||||
// The return value of the export function cosmwasm_api_0_6 is unused and
|
||||
// can be randomized for testing.
|
||||
//
|
||||
// Find position of mutable bytes as follows:
|
||||
// $ wasm-objdump -d contract.wasm | grep -F "cosmwasm_api_0_6" -A 1
|
||||
// 00e67c func[149] <cosmwasm_api_0_6>:
|
||||
// 00e67d: 41 83 0c | i32.const 1539
|
||||
//
|
||||
// In the last line, the addresses 00e67d-00e67f hold a one byte instruction
|
||||
// (https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md#constants-described-here)
|
||||
// and a two byte value (leb128 encoded 1539)
|
||||
|
||||
// Any unsigned integer from 128 to 16383 is encoded to two leb128 bytes
|
||||
const min = 128;
|
||||
const max = 16383;
|
||||
const random = Math.floor(Math.random() * (max - min)) + min;
|
||||
const bytes = leb128Encode(random);
|
||||
|
||||
data[0x00e67d + 1] = bytes[0];
|
||||
data[0x00e67d + 2] = bytes[1];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function makeRandomAddress(): string {
|
||||
return Bech32.encode("cosmos", Random.getBytes(20));
|
||||
}
|
||||
|
||||
async function uploadCustomContract(
|
||||
client: RestClient,
|
||||
pen: Pen,
|
||||
@ -115,15 +84,15 @@ async function uploadCustomContract(
|
||||
gas: "89000000",
|
||||
};
|
||||
|
||||
const account = (await client.authAccounts(faucet.address)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
|
||||
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
|
||||
return client.postTx(marshalTx(signedTx));
|
||||
}
|
||||
|
||||
async function uploadContract(client: RestClient, pen: Pen): Promise<PostTxsResponse> {
|
||||
return uploadCustomContract(client, pen, getRandomizedContract());
|
||||
return uploadCustomContract(client, pen, getRandomizedHackatom());
|
||||
}
|
||||
|
||||
async function instantiateContract(
|
||||
@ -156,8 +125,8 @@ async function instantiateContract(
|
||||
gas: "89000000",
|
||||
};
|
||||
|
||||
const account = (await client.authAccounts(faucet.address)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
|
||||
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
|
||||
return client.postTx(marshalTx(signedTx));
|
||||
@ -188,8 +157,8 @@ async function executeContract(
|
||||
gas: "89000000",
|
||||
};
|
||||
|
||||
const account = (await client.authAccounts(faucet.address)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
|
||||
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
|
||||
return client.postTx(marshalTx(signedTx));
|
||||
@ -289,9 +258,9 @@ describe("RestClient", () => {
|
||||
};
|
||||
|
||||
const client = new RestClient(httpUrl);
|
||||
const account = (await client.authAccounts(faucet.address)).result.value;
|
||||
const { account_number, sequence } = (await client.authAccounts(faucet.address)).result.value;
|
||||
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account);
|
||||
const signBytes = makeSignBytes([theMsg], fee, defaultNetworkId, memo, account_number, sequence);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = makeSignedTx(theMsg, fee, memo, signature);
|
||||
const result = await client.postTx(marshalTx(signedTx));
|
||||
@ -376,7 +345,7 @@ describe("RestClient", () => {
|
||||
const numExisting = existingInfos.length;
|
||||
|
||||
// upload data
|
||||
const wasmCode = getRandomizedContract();
|
||||
const wasmCode = getRandomizedHackatom();
|
||||
const result = await uploadCustomContract(client, pen, wasmCode);
|
||||
expect(result.code).toBeFalsy();
|
||||
const logs = parseLogs(result.logs);
|
||||
|
||||
@ -2,11 +2,11 @@ import { Encoding } from "@iov/encoding";
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
|
||||
import {
|
||||
AminoTx,
|
||||
CodeInfo,
|
||||
ContractInfo,
|
||||
CosmosSdkAccount,
|
||||
isAminoStdTx,
|
||||
CosmosSdkTx,
|
||||
isStdTx,
|
||||
Model,
|
||||
parseWasmData,
|
||||
StdTx,
|
||||
@ -70,7 +70,7 @@ export interface TxsResponse {
|
||||
readonly height: string;
|
||||
readonly txhash: string;
|
||||
readonly raw_log: string;
|
||||
readonly tx: AminoTx;
|
||||
readonly tx: CosmosSdkTx;
|
||||
}
|
||||
|
||||
interface SearchTxsResponse {
|
||||
@ -260,7 +260,7 @@ export class RestClient {
|
||||
public async postTx(tx: Uint8Array): Promise<PostTxsResponse> {
|
||||
// TODO: check this is StdTx
|
||||
const decoded = JSON.parse(fromUtf8(tx));
|
||||
if (!isAminoStdTx(decoded)) {
|
||||
if (!isStdTx(decoded)) {
|
||||
throw new Error("Must be json encoded StdTx");
|
||||
}
|
||||
const params = {
|
||||
|
||||
72
packages/sdk/src/testutils.spec.ts
Normal file
72
packages/sdk/src/testutils.spec.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Random } from "@iov/crypto";
|
||||
import { Bech32, Encoding } from "@iov/encoding";
|
||||
|
||||
import hackatom from "./testdata/contract.json";
|
||||
|
||||
const { fromHex } = Encoding;
|
||||
|
||||
export function leb128Encode(uint: number): Uint8Array {
|
||||
if (uint < 0) throw new Error("Only non-negative values supported");
|
||||
if (uint > 0x7fffffff) throw new Error("Only values in signed int32 range allowed");
|
||||
const out = new Array<number>();
|
||||
let value = uint;
|
||||
do {
|
||||
// tslint:disable: no-bitwise
|
||||
let byte = value & 0b01111111;
|
||||
value >>= 7;
|
||||
|
||||
// more bytes to come: set high order bit of byte
|
||||
if (value !== 0) byte ^= 0b10000000;
|
||||
|
||||
out.push(byte);
|
||||
// tslint:enable: no-bitwise
|
||||
} while (value !== 0);
|
||||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
export function getRandomizedHackatom(): Uint8Array {
|
||||
const data = Encoding.fromBase64(hackatom.data);
|
||||
// The return value of the export function cosmwasm_api_0_6 is unused and
|
||||
// can be randomized for testing.
|
||||
//
|
||||
// Find position of mutable bytes as follows:
|
||||
// $ wasm-objdump -d contract.wasm | grep -F "cosmwasm_api_0_6" -A 1
|
||||
// 00e67c func[149] <cosmwasm_api_0_6>:
|
||||
// 00e67d: 41 83 0c | i32.const 1539
|
||||
//
|
||||
// In the last line, the addresses 00e67d-00e67f hold a one byte instruction
|
||||
// (https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md#constants-described-here)
|
||||
// and a two byte value (leb128 encoded 1539)
|
||||
|
||||
// Any unsigned integer from 128 to 16383 is encoded to two leb128 bytes
|
||||
const min = 128;
|
||||
const max = 16383;
|
||||
const random = Math.floor(Math.random() * (max - min)) + min;
|
||||
const bytes = leb128Encode(random);
|
||||
|
||||
data[0x00e67d + 1] = bytes[0];
|
||||
data[0x00e67d + 2] = bytes[1];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export function makeRandomAddress(): string {
|
||||
return Bech32.encode("cosmos", Random.getBytes(20));
|
||||
}
|
||||
|
||||
describe("leb128", () => {
|
||||
describe("leb128Encode", () => {
|
||||
it("works for single byte values", () => {
|
||||
// Values in 7 bit range are encoded as one byte
|
||||
expect(leb128Encode(0)).toEqual(fromHex("00"));
|
||||
expect(leb128Encode(20)).toEqual(fromHex("14"));
|
||||
expect(leb128Encode(127)).toEqual(fromHex("7f"));
|
||||
});
|
||||
|
||||
it("works for multi byte values", () => {
|
||||
// from external souce (wasm-objdump)
|
||||
expect(leb128Encode(145)).toEqual(fromHex("9101"));
|
||||
expect(leb128Encode(1539)).toEqual(fromHex("830c"));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -2,14 +2,7 @@ import { Encoding } from "@iov/encoding";
|
||||
|
||||
const { fromBase64, fromHex } = Encoding;
|
||||
|
||||
// We will move all needed *interfaces* from amino-js here
|
||||
// This means bcp can just import them from here (if needed at all)
|
||||
export interface Tx {
|
||||
readonly type: string;
|
||||
// TODO
|
||||
readonly value: unknown;
|
||||
}
|
||||
|
||||
/** An Amino/Cosmos SDK StdTx */
|
||||
export interface StdTx {
|
||||
readonly msg: ReadonlyArray<Msg>;
|
||||
readonly fee: StdFee;
|
||||
@ -17,15 +10,18 @@ export interface StdTx {
|
||||
readonly memo: string | undefined;
|
||||
}
|
||||
|
||||
export type AminoTx = Tx & { readonly value: StdTx };
|
||||
|
||||
export function isAminoStdTx(txValue: unknown): txValue is StdTx {
|
||||
export function isStdTx(txValue: unknown): txValue is StdTx {
|
||||
const { memo, msg, fee, signatures } = txValue as StdTx;
|
||||
return (
|
||||
typeof memo === "string" && Array.isArray(msg) && typeof fee === "object" && Array.isArray(signatures)
|
||||
);
|
||||
}
|
||||
|
||||
export interface CosmosSdkTx {
|
||||
readonly type: string;
|
||||
readonly value: StdTx;
|
||||
}
|
||||
|
||||
interface MsgTemplate {
|
||||
readonly type: string;
|
||||
readonly value: object;
|
||||
@ -164,9 +160,6 @@ export interface CosmosSdkAccount {
|
||||
readonly sequence: number;
|
||||
}
|
||||
|
||||
/** The data we need from CosmosSdkAccount to create a nonce */
|
||||
export type NonceInfo = Pick<CosmosSdkAccount, "account_number" | "sequence">;
|
||||
|
||||
export interface CodeInfo {
|
||||
readonly id: number;
|
||||
/** Bech32 account address */
|
||||
|
||||
49
packages/sdk/types/cosmwasmclient.d.ts
vendored
Normal file
49
packages/sdk/types/cosmwasmclient.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
import { Log } from "./logs";
|
||||
import { Coin, StdSignature } from "./types";
|
||||
export interface SigningCallback {
|
||||
(signBytes: Uint8Array): Promise<StdSignature>;
|
||||
}
|
||||
export interface GetNonceResult {
|
||||
readonly accountNumber: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
export interface PostTxResult {
|
||||
readonly logs: readonly Log[];
|
||||
readonly rawLog: string;
|
||||
/** Transaction hash (might be used as transaction ID). Guaranteed to be non-exmpty upper-case hex */
|
||||
readonly transactionHash: string;
|
||||
}
|
||||
export interface ExecuteResult {
|
||||
readonly logs: readonly Log[];
|
||||
}
|
||||
export declare class CosmWasmClient {
|
||||
static makeReadOnly(url: string): CosmWasmClient;
|
||||
static makeWritable(url: string, senderAddress: string, signCallback: SigningCallback): CosmWasmClient;
|
||||
private readonly restClient;
|
||||
private readonly signingData;
|
||||
private get senderAddress();
|
||||
private get signCallback();
|
||||
private constructor();
|
||||
chainId(): Promise<string>;
|
||||
/**
|
||||
* Returns account number and sequence.
|
||||
*
|
||||
* @param address returns data for this address. When unset, the client's sender adddress is used.
|
||||
*/
|
||||
getNonce(address?: string): Promise<GetNonceResult>;
|
||||
postTx(tx: Uint8Array): Promise<PostTxResult>;
|
||||
/** Uploads code and returns a code ID */
|
||||
upload(wasmCode: Uint8Array, memo?: string): Promise<number>;
|
||||
instantiate(
|
||||
codeId: number,
|
||||
initMsg: object,
|
||||
memo?: string,
|
||||
transferAmount?: readonly Coin[],
|
||||
): Promise<string>;
|
||||
execute(
|
||||
contractAddress: string,
|
||||
handleMsg: object,
|
||||
memo?: string,
|
||||
transferAmount?: readonly Coin[],
|
||||
): Promise<ExecuteResult>;
|
||||
}
|
||||
5
packages/sdk/types/encoding.d.ts
vendored
5
packages/sdk/types/encoding.d.ts
vendored
@ -1,10 +1,11 @@
|
||||
import { Msg, NonceInfo, StdFee, StdSignature, StdTx } from "./types";
|
||||
import { Msg, 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,
|
||||
accountNumber: number,
|
||||
sequence: number,
|
||||
): Uint8Array;
|
||||
export declare function encodeSecp256k1Signature(pubkey: Uint8Array, signature: Uint8Array): StdSignature;
|
||||
|
||||
1
packages/sdk/types/index.d.ts
vendored
1
packages/sdk/types/index.d.ts
vendored
@ -5,6 +5,7 @@ export { CosmosAddressBech32Prefix, encodeAddress, isValidAddress } from "./addr
|
||||
export { unmarshalTx } from "./decoding";
|
||||
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
|
||||
export { RestClient, TxsResponse } from "./restclient";
|
||||
export { CosmWasmClient, ExecuteResult, GetNonceResult, PostTxResult } from "./cosmwasmclient";
|
||||
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
|
||||
export {
|
||||
CosmosPubkeyBech32Prefix,
|
||||
|
||||
4
packages/sdk/types/restclient.d.ts
vendored
4
packages/sdk/types/restclient.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { AminoTx, CodeInfo, ContractInfo, CosmosSdkAccount, Model, StdTx } from "./types";
|
||||
import { CodeInfo, ContractInfo, CosmosSdkAccount, CosmosSdkTx, Model, StdTx } from "./types";
|
||||
interface NodeInfo {
|
||||
readonly network: string;
|
||||
}
|
||||
@ -42,7 +42,7 @@ export interface TxsResponse {
|
||||
readonly height: string;
|
||||
readonly txhash: string;
|
||||
readonly raw_log: string;
|
||||
readonly tx: AminoTx;
|
||||
readonly tx: CosmosSdkTx;
|
||||
}
|
||||
interface SearchTxsResponse {
|
||||
readonly total_count: string;
|
||||
|
||||
14
packages/sdk/types/types.d.ts
vendored
14
packages/sdk/types/types.d.ts
vendored
@ -1,17 +1,15 @@
|
||||
export interface Tx {
|
||||
readonly type: string;
|
||||
readonly value: unknown;
|
||||
}
|
||||
/** An Amino/Cosmos SDK StdTx */
|
||||
export interface StdTx {
|
||||
readonly msg: ReadonlyArray<Msg>;
|
||||
readonly fee: StdFee;
|
||||
readonly signatures: ReadonlyArray<StdSignature>;
|
||||
readonly memo: string | undefined;
|
||||
}
|
||||
export declare type AminoTx = Tx & {
|
||||
export declare function isStdTx(txValue: unknown): txValue is StdTx;
|
||||
export interface CosmosSdkTx {
|
||||
readonly type: string;
|
||||
readonly value: StdTx;
|
||||
};
|
||||
export declare function isAminoStdTx(txValue: unknown): txValue is StdTx;
|
||||
}
|
||||
interface MsgTemplate {
|
||||
readonly type: string;
|
||||
readonly value: object;
|
||||
@ -118,8 +116,6 @@ export interface CosmosSdkAccount {
|
||||
readonly account_number: number;
|
||||
readonly sequence: number;
|
||||
}
|
||||
/** The data we need from CosmosSdkAccount to create a nonce */
|
||||
export declare type NonceInfo = Pick<CosmosSdkAccount, "account_number" | "sequence">;
|
||||
export interface CodeInfo {
|
||||
readonly id: number;
|
||||
/** Bech32 account address */
|
||||
|
||||
@ -1,141 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
const { Encoding } = require("@iov/encoding");
|
||||
const {
|
||||
encodeSecp256k1Signature,
|
||||
makeSignBytes,
|
||||
marshalTx,
|
||||
logs,
|
||||
RestClient,
|
||||
Secp256k1Pen,
|
||||
} = require("@cosmwasm/sdk");
|
||||
const { CosmWasmClient, encodeSecp256k1Signature, Secp256k1Pen } = require("@cosmwasm/sdk");
|
||||
const fs = require("fs");
|
||||
|
||||
const httpUrl = "http://localhost:1317";
|
||||
const networkId = "testing";
|
||||
const defaultFee = {
|
||||
amount: [
|
||||
{
|
||||
amount: "5000",
|
||||
denom: "ucosm",
|
||||
},
|
||||
],
|
||||
gas: "1000000", // one million
|
||||
const faucet = {
|
||||
mnemonic:
|
||||
"economy stock theory fatal elder harbor betray wasp final emotion task crumble siren bottom lizard educate guess current outdoor pair theory focus wife stone",
|
||||
address: "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6",
|
||||
};
|
||||
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 faucetAddress = "cosmos1pkptre7fdkl6gfrzlesjjvhxhlc3r4gmmk8rs6";
|
||||
const unusedAccount = "cosmos1cjsxept9rkggzxztslae9ndgpdyt2408lk850u";
|
||||
|
||||
async function uploadContract(client, pen, wasm) {
|
||||
const memo = "Upload ERC20 contract";
|
||||
const storeCodeMsg = {
|
||||
type: "wasm/store-code",
|
||||
value: {
|
||||
sender: faucetAddress,
|
||||
wasm_byte_code: Encoding.toBase64(wasm),
|
||||
source: "",
|
||||
builder: "",
|
||||
const initMsgAsh = {
|
||||
decimals: 5,
|
||||
name: "Ash token",
|
||||
symbol: "ASH",
|
||||
initial_balances: [
|
||||
{
|
||||
address: faucet.address,
|
||||
amount: "11",
|
||||
},
|
||||
};
|
||||
|
||||
const account = (await client.authAccounts(faucetAddress)).result.value;
|
||||
const signBytes = makeSignBytes([storeCodeMsg], defaultFee, networkId, memo, account);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = {
|
||||
msg: [storeCodeMsg],
|
||||
fee: defaultFee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
return client.postTx(marshalTx(signedTx));
|
||||
}
|
||||
|
||||
async function instantiateContract(client, pen, codeId, msg, transferAmount) {
|
||||
const memo = "Create an ERC20 instance";
|
||||
const instantiateContractMsg = {
|
||||
type: "wasm/instantiate",
|
||||
value: {
|
||||
sender: faucetAddress,
|
||||
code_id: codeId.toString(),
|
||||
init_msg: msg,
|
||||
init_funds: transferAmount || [],
|
||||
{
|
||||
address: unusedAccount,
|
||||
amount: "12812345",
|
||||
},
|
||||
};
|
||||
const account = (await client.authAccounts(faucetAddress)).result.value;
|
||||
const signBytes = makeSignBytes([instantiateContractMsg], defaultFee, networkId, memo, account);
|
||||
const signature = encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
const signedTx = {
|
||||
msg: [instantiateContractMsg],
|
||||
fee: defaultFee,
|
||||
memo: memo,
|
||||
signatures: [signature],
|
||||
};
|
||||
return client.postTx(marshalTx(signedTx));
|
||||
}
|
||||
],
|
||||
};
|
||||
const initMsgBash = {
|
||||
decimals: 0,
|
||||
name: "Bash Token",
|
||||
symbol: "BASH",
|
||||
initial_balances: [
|
||||
{
|
||||
address: faucet.address,
|
||||
amount: "999999999",
|
||||
},
|
||||
{
|
||||
address: unusedAccount,
|
||||
amount: "42",
|
||||
},
|
||||
],
|
||||
};
|
||||
const initMsgCash = {
|
||||
decimals: 18,
|
||||
name: "Cash Token",
|
||||
symbol: "CASH",
|
||||
initial_balances: [
|
||||
{
|
||||
address: faucet.address,
|
||||
amount: "189189189000000000000000000", // 189189189 CASH
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
|
||||
const client = new RestClient(httpUrl);
|
||||
const pen = await Secp256k1Pen.fromMnemonic(faucet.mnemonic);
|
||||
const client = CosmWasmClient.makeWritable(httpUrl, faucet.address, async signBytes => {
|
||||
return encodeSecp256k1Signature(pen.pubkey, await pen.createSignature(signBytes));
|
||||
});
|
||||
|
||||
const wasm = fs.readFileSync(__dirname + "/contracts/cw-erc20.wasm");
|
||||
const uploadResult = await uploadContract(client, pen, wasm);
|
||||
if (uploadResult.code) {
|
||||
throw new Error(`Uploading failed with code: ${uploadResult.code}; log: '${uploadResult.raw_log}'`);
|
||||
}
|
||||
const codeIdAttr = logs.findAttribute(logs.parseLogs(uploadResult.logs), "message", "code_id");
|
||||
const codeId = Number.parseInt(codeIdAttr.value, 10);
|
||||
const codeId = await client.upload(wasm, "Upload ERC20 contract");
|
||||
console.info(`Upload succeeded. Code ID is ${codeId}`);
|
||||
|
||||
const initMsgAsh = {
|
||||
decimals: 5,
|
||||
name: "Ash token",
|
||||
symbol: "ASH",
|
||||
initial_balances: [
|
||||
{
|
||||
address: faucetAddress,
|
||||
amount: "11",
|
||||
},
|
||||
{
|
||||
address: unusedAccount,
|
||||
amount: "12812345",
|
||||
},
|
||||
],
|
||||
};
|
||||
const initMsgBash = {
|
||||
decimals: 0,
|
||||
name: "Bash Token",
|
||||
symbol: "BASH",
|
||||
initial_balances: [
|
||||
{
|
||||
address: faucetAddress,
|
||||
amount: "999999999",
|
||||
},
|
||||
{
|
||||
address: unusedAccount,
|
||||
amount: "42",
|
||||
},
|
||||
],
|
||||
};
|
||||
const initMsgCash = {
|
||||
decimals: 18,
|
||||
name: "Cash Token",
|
||||
symbol: "CASH",
|
||||
initial_balances: [
|
||||
{
|
||||
address: faucetAddress,
|
||||
amount: "189189189000000000000000000", // 189189189 CASH
|
||||
},
|
||||
],
|
||||
};
|
||||
for (const initMsg of [initMsgAsh, initMsgBash, initMsgCash]) {
|
||||
const initResult = await instantiateContract(client, pen, codeId, initMsg);
|
||||
if (initResult.code) {
|
||||
throw new Error(`Instantiation failed with code: ${initResult.code}; log: '${initResult.raw_log}'`);
|
||||
}
|
||||
const instantiationLogs = logs.parseLogs(initResult.logs);
|
||||
const contractAddress = logs.findAttribute(instantiationLogs, "message", "contract_address").value;
|
||||
const memo = `Create an ERC20 instance for ${initMsg.symbol}`;
|
||||
const contractAddress = await client.instantiate(codeId, initMsg, memo);
|
||||
console.info(`Contract instantiated for ${initMsg.symbol} at ${contractAddress}`);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user