Merge pull request #62 from confio/upload-contract
Add script to deploy ERC20 contract for local development
This commit is contained in:
commit
3207a5d5ed
@ -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
|
||||
|
||||
@ -41,6 +41,7 @@ module.exports = {
|
||||
files: "**/*.js",
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
3
packages/sdk/types/index.d.ts
vendored
3
packages/sdk/types/index.d.ts
vendored
@ -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 };
|
||||
|
||||
11
packages/sdk/types/logs.d.ts
vendored
11
packages/sdk/types/logs.d.ts
vendored
@ -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;
|
||||
|
||||
8
packages/sdk/types/types.d.ts
vendored
8
packages/sdk/types/types.d.ts
vendored
@ -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;
|
||||
};
|
||||
}
|
||||
/**
|
||||
|
||||
1
scripts/cosm/contracts/checksums.sha256
Normal file
1
scripts/cosm/contracts/checksums.sha256
Normal file
@ -0,0 +1 @@
|
||||
b26861a6aa9858585ed905a590272735bd4fe8177c708940236224e8c9ff73ca cw-erc20.wasm
|
||||
BIN
scripts/cosm/contracts/cw-erc20.wasm
Normal file
BIN
scripts/cosm/contracts/cw-erc20.wasm
Normal file
Binary file not shown.
122
scripts/cosm/deploy_erc20.js
Executable file
122
scripts/cosm/deploy_erc20.js
Executable 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);
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue
Block a user