diff --git a/.gitignore b/.gitignore index 66c5b99a..cae95ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ node_modules/ # IDE-specific .vscode/ + +# demo mnemonics from cli +*.key diff --git a/packages/cli/README.md b/packages/cli/README.md index 2277eaa1..54236361 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -81,55 +81,169 @@ The above code shows you the use of the API and various objects and is a great w how to embed cosmwasm-js into your project. However, if you just want a cli to perform some quick queries on a chain, you can use an extended set of helpers: -1. Start a local wasmd blockchain, for example running the setup from `../../scripts/wasmd/start.sh` -2. Start with `./bin/cosmwasm-cli --init examples/helpers.ts` (note the new init file) -3. Deploy some erc20 contracts: `../../scripts/wasmd/init.sh` -4. Play around as in the following example code +Start with `./bin/cosmwasm-cli --init examples/helpers.ts` + +(This points to the Demo Net at https://lcd.demo-07.cosmwasm.com for ease of use. Other networks, look below) + +Setup Account: ```ts -const account = (await client.authAccounts(faucetAddress)).result.value; -account +// you can hand-copy a mnemonic here, but this is easiest for reuse between sessions +// it creates a random one first time, then reads it in the future +const mnemonic = loadOrCreateMnemonic("foo.key"); +const {address, client} = await connect(mnemonic, {}); +address +client.getAccount(); +// if empty - this only works with CosmWasm +hitFaucet(defaultFaucetUrl, address, "COSM") +client.getAccount(); +``` + +View contracts: + +```ts // show all code and contracts -client.listCodeInfo() -client.listContractAddresses() +client.getCodes() -// query the first contract -const addr = (await client.listContractAddresses())[0] -const info = await client.getContractInfo(addr) -info.init_msg +// query the first contract for first code +const contracts = await client.getContracts(1); +contracts +const info = await client.getContract(contracts[0].address) +info +info.initMsg // see your balance here -smartQuery(client, addr, { balance: { address: faucetAddress } }) - -// make a new contract -const initMsg = { name: "Foo Coin", symbol: "FOO", decimals: 2, initial_balances: [{address: faucetAddress, amount: "123456789"}]} -const foo = await instantiateContract(client, pen, 1, initMsg); - -smartQuery(client, foo, { balance: { address: faucetAddress } }) - -const rcpt = await randomAddress(); -rcpt -smartQuery(client, foo, { balance: { address: rcpt } }) - -const execMsg = { transfer: {recipient: rcpt, amount: "808"}} -const exec = await executeContract(client, pen, foo, execMsg); -exec -exec[0].events[0] -smartQuery(client, foo, { balance: { address: rcpt } }) +smartQuery(client, addr, { balance: { address } }) ``` -## Other example codes - -### Create random mnemonic and Cosmos address +Instantiate and use ERC20 contract: ```ts -const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); -const pen = await Secp256k1Pen.fromMnemonic(mnemonic); -const pubkey = encodeSecp256k1Pubkey(pen.pubkey); -const address = pubkeyToAddress(pubkey, "cosmos"); +// no money? no problem. +// let's make our own s**coin - replace "FOO" with something else to avoid duplicates +const initMsg = { name: "Foo Coin", symbol: "FOO", decimals: 2, initial_balances: [{address, amount: "123456789"}]} +const foo = await client.instantiate(1, initMsg, "FOO"); +foo +foo.logs[0].events[0] +const fooAddr = foo.contractAddress; + +// we can also find this another way... +const fooAddr2 = await client.getContracts(1).then(contracts => contracts.filter(x => x.label == "FOO").map(x => x.address)[0]) +[fooAddr, fooAddr2] + +// now we have some cash +smartQuery(client, fooAddr, { balance: { address } }) + +const rcpt = await randomAddress("cosmos"); +rcpt +smartQuery(client, fooAddr, { balance: { address: rcpt } }) + +const execMsg = { transfer: {recipient: rcpt, amount: "808"}} +const exec = await client.execute(fooAddr, execMsg); +exec +exec.logs[0].events[0] +smartQuery(client, fooAddr, { balance: { address: rcpt } }) ``` +Or just send tokens: + +```ts +client.getAccount(rcpt) + +const sent = await client.sendTokens(rcpt, [{amount: "1234", denom: "ucosm"}]) +sent +foo.logs[0].events[0] +``` + +### Use Custom Network + +All the network info can be configured inside the last argument to connect. +To see how to connect to the Regen Testnet, try this. (Note you need to use `.editor` +in the repl to allow multi-line commands. Alternative is to place entire `regenOptions` +on one line. + +Run `./bin/cosmwasm-cli --init examples/helpers.ts` + +```ts +.editor +const regenOptions = { + httpUrl: "https://regen-lcd.vitwit.com/", + networkId: "kontraua", + feeToken: "utree", + gasPrice: 0.025, + bech32prefix: "xrn:", +} +^D + +const mnemonic = loadOrCreateMnemonic("regen.key"); +const {address, client} = await connect(mnemonic, regenOptions); +address + +// check some random genesis account +client.getAccount("xrn:1pdfr7xuckj6lhdphdde6peres9ufwgpsv87mag") + +// your own account is undefined unless you did this before +client.getAccount() +``` + +Hit the faucet with your address (in browser): https://regen.vitwit.com/faucet then continue in node + +```ts +// should have tokens now +client.getAccount() +``` + +At this point you can continue all the other behaviors from above, looking at codes, etc. +Do note that the ERC contract is code `5` on this network (instead of `1` above). + +### Importing keys from `wasmcli` + +If you are using the go commands and have tokens there, you may want to reuse the same account. +(If you don't know what this is, just skip this section). You can reuse the mnemonic between the +Go tooling and the Node.js tooling, but this violates all security protocols - only use this for +testnets. In the future we will offer proper encrypted key management for CosmWasm-JS. + +(You can replace `wasmcli` with `xrncli` and use `regenOptions` if you wish to use that testnet) + +Create a new key - note mnemonic and address + +```sh +$ wasmcli keys add demo2 + +- name: demo2 + type: local + address: cosmos1d4ut3z9c0kplgz5ma9t6ee637tagjqfyu4sxyl + pubkey: cosmospub1addwnpepqtagg2smk2zvj77xaslej2wevwz7jft0q5hj5yuwvek3r6z0ufjtxnde4rq + mnemonic: "" + threshold: 0 + pubkeys: [] + + +**Important** write this mnemonic phrase in a safe place. +It is the only way to recover your account if you ever forget your password. + +cousin nephew vintage label empty sunny cargo mushroom photo side clarify sleep solid entire deal tattoo vehicle record discover arrive sting staff salt uncle +``` + +Save mnemonic to a file + +```sh +echo "cousin nephew vintage label empty sunny cargo mushroom photo side clarify sleep solid entire deal tattoo vehicle record discover arrive sting staff salt uncle" > wasmcli.key +``` + +Load it up in cosmwasm-js: `./bin/cosmwasm-cli --init examples/helpers.ts` + +```ts +const mnemonic = loadOrCreateMnemonic("wasmcli.key"); +const {address, client} = await connect(mnemonic, regenOptions); + +// this should match what you got on the cli - showing compatibility +address +``` + +Once you have access to the same key as in the cli, you can use those tokens to play with contracts + ## License This package is part of the cosmwasm-js repository, licensed under the Apache diff --git a/packages/cli/examples/helpers.ts b/packages/cli/examples/helpers.ts index 6b414455..9db4aed9 100644 --- a/packages/cli/examples/helpers.ts +++ b/packages/cli/examples/helpers.ts @@ -1,101 +1,95 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { - logs -} from "@cosmwasm/sdk"; - -const defaultHttpUrl = "http://localhost:1317"; -const defaultNetworkId = "testing"; -const defaultFee: types.StdFee = { - amount: [ - { - amount: "5000", - denom: "ucosm", - }, - ], - gas: "890000", +interface Options { + httpUrl: string; + networkId: string; + feeToken: string; + gasPrice: number; + bech32prefix: string; }; -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 pen = await Secp256k1Pen.fromMnemonic(faucetMnemonic); -const client = new RestClient(defaultHttpUrl); - -const networkId = "testing"; - - -// helper functions -const instantiateContract = async (initClient: RestClient, initPen: Secp256k1Pen, codeId: number, msg: object, transferAmount?: types.Coin[]): Promise => { - const memo = "Create an ERC20 instance"; - const sender = pubkeyToAddress({ "type": types.pubkeyType.secp256k1, "value": toBase64(initPen.pubkey)}, "cosmos"); - const instantiateContractMsg = { - type: "wasm/instantiate", - value: { - sender: sender, - code_id: codeId.toString(), - init_msg: msg, - init_funds: transferAmount || [], - }, - }; - const account = (await initClient.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([instantiateContractMsg], defaultFee, networkId, memo, account); - const signature = await initPen.sign(signBytes); - const signedTx = { - msg: [instantiateContractMsg], - fee: defaultFee, - memo: memo, - signatures: [signature], - }; - const result = await initClient.postTx(signedTx); - if (result.code) { - throw new Error(`Failed tx: (${result.code}): ${result.raw_log}`) - } - const instantiationLogs = logs.parseLogs(result.logs); - const contractAddress = logs.findAttribute(instantiationLogs, "message", "contract_address").value; - return contractAddress; +const defaultOptions: Options = { + httpUrl: "https://lcd.demo-07.cosmwasm.com", + networkId: "testing", + feeToken: "ucosm", + gasPrice: 0.025, + bech32prefix: "cosmos", } -// helper functions -const executeContract = async (execClient: RestClient, execPen: Secp256k1Pen, contractAddr: string, msg: object, transferAmount?: types.Coin[]): Promise => { - const memo = "Create an ERC20 instance"; - const sender = pubkeyToAddress({ "type": types.pubkeyType.secp256k1, "value": toBase64(execPen.pubkey)}, "cosmos"); - const instantiateContractMsg = { - type: "wasm/execute", - value: { - sender: sender, - contract: contractAddr, - msg: msg, - sent_funds: transferAmount || [], - }, +const defaultFaucetUrl = "https://faucet.demo-07.cosmwasm.com/credit"; + +const buildFeeTable = (feeToken: string, gasPrice: number): FeeTable => { + const stdFee = (gas: number, denom: string, price: number) => { + const amount = Math.floor(gas * price); + return { + amount: [{ amount: amount.toString(), denom: denom }], + gas: gas.toString(), + } }; - const account = (await execClient.authAccounts(faucetAddress)).result.value; - const signBytes = makeSignBytes([instantiateContractMsg], defaultFee, networkId, memo, account); - const signature = await execPen.sign(signBytes); - const signedTx = { - msg: [instantiateContractMsg], - fee: defaultFee, - memo: memo, - signatures: [signature], - }; - const result = await execClient.postTx(signedTx); - if (result.code) { - throw new Error(`Failed tx: (${result.code}): ${result.raw_log}`) + + return { + upload: stdFee(1000000, feeToken, gasPrice), + init: stdFee(500000, feeToken, gasPrice), + exec: stdFee(200000, feeToken, gasPrice), + send: stdFee(80000, feeToken, gasPrice), } - const execLogs = logs.parseLogs(result.logs); - return execLogs; +}; + +// TODO: hit faucet +// if (config.faucetUrl) { +// const acct = await client.getAccount(); +// if (!acct?.balance?.length) { +// await ky.post(config.faucetUrl, { json: { ticker: "COSM", address } }); +// } +// } + +const penAddress = (pen: Secp256k1Pen, prefix: string): string => { + const pubkey = encodeSecp256k1Pubkey(pen.pubkey); + return pubkeyToAddress(pubkey, prefix); } +const connect = async (mnemonic: string, opts: Partial): Promise<{ + client: SigningCosmWasmClient, + address: string, +}> => { + const options: Options = {...defaultOptions, ...opts}; + const feeTable = buildFeeTable(options.feeToken, options.gasPrice); + const pen = await Secp256k1Pen.fromMnemonic(mnemonic); + const address = penAddress(pen, options.bech32prefix); + + const client = new SigningCosmWasmClient(options.httpUrl, address, signBytes => pen.sign(signBytes), feeTable); + return {client, address}; +}; + // smartQuery assumes the content is proper JSON data and parses before returning it -const smartQuery = async (client: RestClient, addr: string, query: object): Promise => { +const smartQuery = async (client: CosmWasmClient, addr: string, query: object): Promise => { const bin = await client.queryContractSmart(addr, query); return JSON.parse(fromUtf8(bin)); } - -const randomAddress = async (): Promise => { - const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); - const randomPen = await Secp256k1Pen.fromMnemonic(mnemonic); - const pubkey = encodeSecp256k1Pubkey(randomPen.pubkey); - return pubkeyToAddress(pubkey, "cosmos"); +// loadOrCreateMnemonic will try to load a mnemonic from the file. +// If missing, it will generate a random one and save to the file. +// +// This is not secure, but does allow simple developer access to persist a +// mnemonic between sessions +const loadOrCreateMnemonic = (filename: string): string => { + try { + const mnemonic = fs.readFileSync(filename, "utf8"); + return mnemonic.trim(); + } catch (err) { + const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); + fs.writeFileSync(filename, mnemonic, "utf8"); + return mnemonic; + } +} + +const hitFaucet = async (faucetUrl: string, address: string, ticker: string): Promise => { + const r = await axios.post(defaultFaucetUrl, { ticker, address }); + console.log(r.status); + console.log(r.data); +} + +const randomAddress = async (prefix: string): Promise => { + const mnemonic = Bip39.encode(Random.getBytes(16)).toString(); + const pen = await Secp256k1Pen.fromMnemonic(mnemonic); + const pubkey = encodeSecp256k1Pubkey(pen.pubkey); + return pubkeyToAddress(pubkey, prefix); }