Merge pull request #62 from confio/upload-contract

Add script to deploy ERC20 contract for local development
This commit is contained in:
Simon Warta 2020-02-07 17:37:56 +01:00 committed by GitHub
commit 3207a5d5ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 195 additions and 47 deletions

View File

@ -83,6 +83,9 @@ jobs:
- run:
name: Wait for blockchain and REST server to be ready (started in background)
command: timeout 60 bash -c "until curl -s http://localhost:1317/node_info > /dev/null; do sleep 1; done"
- run:
name: Deploy ERC20 contract
command: ./scripts/cosm/deploy_erc20.js
- run:
environment:
COSMOS_ENABLED: 1

View File

@ -41,6 +41,7 @@ module.exports = {
files: "**/*.js",
rules: {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off",
},
},
{

View File

@ -1,8 +1,9 @@
import * as logs from "./logs";
import * as types from "./types";
export { logs, types };
export { CosmosBech32Prefix, decodeBech32Pubkey, encodeAddress, isValidAddress } from "./address";
export { unmarshalTx } from "./decoding";
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
export { RestClient, TxsResponse } from "./restclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export { types };

View File

@ -59,3 +59,26 @@ export function parseLogs(input: unknown): readonly Log[] {
if (!Array.isArray(input)) throw new Error("Logs must be an array");
return input.map(parseLog);
}
/**
* Searches in logs for the first event of the given event type and in that event
* for the first first attribute with the given attribute key.
*
* Throws if the attribute was not found.
*/
export function findAttribute(
logs: readonly Log[],
eventType: "message" | "transfer",
attrKey: string,
): Attribute {
const firstLogs = logs.find(() => true);
const out = firstLogs?.events
.find(event => event.type === eventType)
?.attributes.find(attr => attr.key === attrKey);
if (!out) {
throw new Error(
`Could not find attribute '${attrKey}' in first event of type '${eventType}' in first log.`,
);
}
return out;
}

View File

@ -4,7 +4,7 @@ import { Bech32, Encoding } from "@iov/encoding";
import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
import { leb128Encode } from "./leb128.spec";
import { Attribute, Log, parseLogs } from "./logs";
import { findAttribute, parseLogs } from "./logs";
import { Pen, Secp256k1Pen } from "./pen";
import { PostTxsResponse, RestClient } from "./restclient";
import contract from "./testdata/contract.json";
@ -36,11 +36,6 @@ function pendingWithoutCosmos(): void {
}
}
function parseSuccess(rawLog?: string): readonly Log[] {
if (!rawLog) throw new Error("Log missing");
return parseLogs(JSON.parse(rawLog));
}
function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: StdSignature): StdTx {
return {
msg: [firstMsg],
@ -80,20 +75,6 @@ function makeRandomAddress(): string {
return Bech32.encode("cosmos", Random.getBytes(20));
}
/** Throws if the attribute was not found */
function findAttribute(logs: readonly Log[], eventType: "message" | "transfer", attrKey: string): Attribute {
const firstLogs = logs.find(() => true);
const out = firstLogs?.events
.find(event => event.type === eventType)
?.attributes.find(attr => attr.key === attrKey);
if (!out) {
throw new Error(
`Could not find attribute '${attrKey}' in first event of type '${eventType}' in first log.`,
);
}
return out;
}
async function uploadContract(client: RestClient, pen: Pen): Promise<PostTxsResponse> {
const memo = "My first contract on chain";
const theMsg: MsgStoreCode = {
@ -127,7 +108,7 @@ async function instantiateContract(
pen: Pen,
codeId: number,
beneficiaryAddress: string,
transferAmount: readonly Coin[],
transferAmount?: readonly Coin[],
): Promise<PostTxsResponse> {
const memo = "Create an escrow instance";
const theMsg: MsgInstantiateContract = {
@ -139,7 +120,7 @@ async function instantiateContract(
verifier: faucetAddress,
beneficiary: beneficiaryAddress,
},
init_funds: transferAmount,
init_funds: transferAmount || [],
},
};
const fee: StdFee = {
@ -291,7 +272,7 @@ describe("RestClient", () => {
// console.log("Raw log:", result.raw_log);
const result = await uploadContract(client, pen);
expect(result.code).toBeFalsy();
const logs = parseSuccess(result.raw_log);
const logs = parseLogs(result.logs);
const codeIdAttr = findAttribute(logs, "message", "code_id");
codeId = Number.parseInt(codeIdAttr.value, 10);
expect(codeId).toBeGreaterThanOrEqual(1);
@ -305,7 +286,7 @@ describe("RestClient", () => {
const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount);
expect(result.code).toBeFalsy();
// console.log("Raw log:", result.raw_log);
const logs = parseSuccess(result.raw_log);
const logs = parseLogs(result.logs);
const contractAddressAttr = findAttribute(logs, "message", "contract_address");
contractAddress = contractAddressAttr.value;
const amountAttr = findAttribute(logs, "transfer", "amount");
@ -320,7 +301,7 @@ describe("RestClient", () => {
const result = await executeContract(client, pen, contractAddress);
expect(result.code).toBeFalsy();
// console.log("Raw log:", result.raw_log);
const [firstLog] = parseSuccess(result.raw_log);
const [firstLog] = parseLogs(result.logs);
expect(firstLog.log).toEqual(`released funds to ${beneficiaryAddress}`);
// Verify token transfer from contract to beneficiary
@ -346,7 +327,7 @@ describe("RestClient", () => {
// upload data
const result = await uploadContract(client, pen);
expect(result.code).toBeFalsy();
const logs = parseSuccess(result.raw_log);
const logs = parseLogs(result.logs);
const codeIdAttr = findAttribute(logs, "message", "code_id");
const codeId = Number.parseInt(codeIdAttr.value, 10);
@ -381,9 +362,9 @@ describe("RestClient", () => {
if (existingInfos.length > 0) {
codeId = existingInfos[existingInfos.length - 1].id;
} else {
const uploaded = await uploadContract(client, pen);
expect(uploaded.code).toBeFalsy();
const uploadLogs = parseSuccess(uploaded.raw_log);
const uploadResult = await uploadContract(client, pen);
expect(uploadResult.code).toBeFalsy();
const uploadLogs = parseLogs(uploadResult.logs);
const codeIdAttr = findAttribute(uploadLogs, "message", "code_id");
codeId = Number.parseInt(codeIdAttr.value, 10);
}
@ -393,7 +374,7 @@ describe("RestClient", () => {
const result = await instantiateContract(client, pen, codeId, beneficiaryAddress, transferAmount);
expect(result.code).toBeFalsy();
const logs = parseSuccess(result.raw_log);
const logs = parseLogs(result.logs);
const contractAddressAttr = findAttribute(logs, "message", "contract_address");
const myAddress = contractAddressAttr.value;
@ -424,13 +405,17 @@ describe("RestClient", () => {
const noContract = makeRandomAddress();
const expectedKey = toAscii("config");
// find an existing contract (created above)
// we assume all contracts on this chain are the same (created by these tests)
const getContractAddress = async (): Promise<string> => {
const contractInfos = await client.listContractAddresses();
expect(contractInfos.length).toBeGreaterThan(0);
return contractInfos[0];
};
/**
* Finds the most recent contract (created above)
*
* We assume the tests above ran, all instantiate the same contract and no other process squeezed in a different contract.
*/
async function getContractAddress(): Promise<string> {
const contracts = Array.from(await client.listContractAddresses());
const last = contracts.reverse().find(() => true);
if (!last) throw new Error("No contract found");
return last;
}
it("can get all state", async () => {
pendingWithoutCosmos();

View File

@ -51,10 +51,10 @@ export interface MsgStoreCode extends MsgTemplate {
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;
/** A valid URI reference to the contract's source code. Can be empty. */
readonly source: string;
/** A docker tag. Can be empty. */
readonly builder: string;
};
}

View File

@ -1,7 +1,8 @@
import * as logs from "./logs";
import * as types from "./types";
export { logs, types };
export { CosmosBech32Prefix, decodeBech32Pubkey, encodeAddress, isValidAddress } from "./address";
export { unmarshalTx } from "./decoding";
export { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding";
export { RestClient, TxsResponse } from "./restclient";
export { makeCosmoshubPath, Pen, PrehashType, Secp256k1Pen } from "./pen";
export { types };

View File

@ -15,3 +15,14 @@ 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[];
/**
* Searches in logs for the first event of the given event type and in that event
* for the first first attribute with the given attribute key.
*
* Throws if the attribute was not found.
*/
export declare function findAttribute(
logs: readonly Log[],
eventType: "message" | "transfer",
attrKey: string,
): Attribute;

View File

@ -39,10 +39,10 @@ export interface MsgStoreCode extends MsgTemplate {
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;
/** A valid URI reference to the contract's source code. Can be empty. */
readonly source: string;
/** A docker tag. Can be empty. */
readonly builder: string;
};
}
/**

View File

@ -0,0 +1 @@
b26861a6aa9858585ed905a590272735bd4fe8177c708940236224e8c9ff73ca cw-erc20.wasm

Binary file not shown.

122
scripts/cosm/deploy_erc20.js Executable file
View File

@ -0,0 +1,122 @@
#!/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 fs = require("fs");
const httpUrl = "http://localhost:1317";
const networkId = "testing";
const defaultFee = {
amount: [
{
amount: "5000",
denom: "ucosm",
},
],
gas: "1000000", // one million
};
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";
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 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 || [],
},
};
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));
}
async function main() {
const pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic);
const client = new RestClient(httpUrl);
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);
console.info(`Upload succeeded. Code ID is ${codeId}`);
const initMsg = {
decimals: 5,
name: "Ash token",
symbol: "ASH",
initial_balances: [
{
address: faucetAddress,
amount: "11",
},
],
};
const instantiationResult = await instantiateContract(client, pen, codeId, initMsg);
if (instantiationResult.code) {
throw new Error(
`Instantiation failed with code: ${instantiationResult.code}; log: '${instantiationResult.raw_log}'`,
);
}
const instantiationLogs = logs.parseLogs(instantiationResult.logs);
const contractAddress = logs.findAttribute(instantiationLogs, "message", "contract_address").value;
console.info(`Contract instantiated at ${contractAddress}`);
}
main().then(
() => {
console.info("All done, let the coins flow.");
process.exit(0);
},
error => {
console.error(error);
process.exit(1);
},
);