diff --git a/.circleci/config.yml b/.circleci/config.yml index b12f1baa..5d85d39f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/.eslintrc.js b/.eslintrc.js index a1dee715..52a62083 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,6 +41,7 @@ module.exports = { files: "**/*.js", rules: { "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/explicit-function-return-type": "off", }, }, { diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 8ac4d6f7..271e6a18 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -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 }; diff --git a/packages/sdk/src/logs.ts b/packages/sdk/src/logs.ts index fd96e3df..79d7b915 100644 --- a/packages/sdk/src/logs.ts +++ b/packages/sdk/src/logs.ts @@ -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; +} diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index e3f9541b..a093c0f1 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -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 { 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 { 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 => { - 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 { + 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(); diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 56da7b1a..5dfd60aa 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -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; }; } diff --git a/packages/sdk/types/index.d.ts b/packages/sdk/types/index.d.ts index a341b70c..084a2cfb 100644 --- a/packages/sdk/types/index.d.ts +++ b/packages/sdk/types/index.d.ts @@ -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 }; diff --git a/packages/sdk/types/logs.d.ts b/packages/sdk/types/logs.d.ts index 871c848c..9a9e92c7 100644 --- a/packages/sdk/types/logs.d.ts +++ b/packages/sdk/types/logs.d.ts @@ -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; diff --git a/packages/sdk/types/types.d.ts b/packages/sdk/types/types.d.ts index 2d26a1b3..c2d760a3 100644 --- a/packages/sdk/types/types.d.ts +++ b/packages/sdk/types/types.d.ts @@ -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; }; } /** diff --git a/scripts/cosm/contracts/checksums.sha256 b/scripts/cosm/contracts/checksums.sha256 new file mode 100644 index 00000000..b36dcaa7 --- /dev/null +++ b/scripts/cosm/contracts/checksums.sha256 @@ -0,0 +1 @@ +b26861a6aa9858585ed905a590272735bd4fe8177c708940236224e8c9ff73ca cw-erc20.wasm diff --git a/scripts/cosm/contracts/cw-erc20.wasm b/scripts/cosm/contracts/cw-erc20.wasm new file mode 100644 index 00000000..04512932 Binary files /dev/null and b/scripts/cosm/contracts/cw-erc20.wasm differ diff --git a/scripts/cosm/deploy_erc20.js b/scripts/cosm/deploy_erc20.js new file mode 100755 index 00000000..9a6791d6 --- /dev/null +++ b/scripts/cosm/deploy_erc20.js @@ -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); + }, +);