From 0a3990465cb9bedd72f9ee05ca63182661c73940 Mon Sep 17 00:00:00 2001 From: Jacques Wagener Date: Sat, 21 Dec 2019 00:37:40 +0200 Subject: [PATCH] Add basic test layout. --- Makefile | 10 +- nimplay/substrate_runtime.nim | 12 +- tests/substrate/test.sh | 22 ++++ tests/substrate/tests/consts.ts | 9 ++ .../substrate/tests/contract-nimplay.spec.ts | 98 ++++++++++++++++ tests/substrate/tests/utils.ts | 109 ++++++++++++++++++ tests/substrate/tsconfig.js | 24 ++++ tests/substrate/utils.ts | 9 ++ 8 files changed, 289 insertions(+), 4 deletions(-) create mode 100755 tests/substrate/test.sh create mode 100644 tests/substrate/tests/consts.ts create mode 100644 tests/substrate/tests/contract-nimplay.spec.ts create mode 100644 tests/substrate/tests/utils.ts create mode 100644 tests/substrate/tsconfig.js create mode 100644 tests/substrate/utils.ts diff --git a/Makefile b/Makefile index b58d06a..7b73ccd 100644 --- a/Makefile +++ b/Makefile @@ -81,8 +81,16 @@ ee-examples: $(WASM32_NIMC) --out:examples/ee/helloworld.wasm examples/ee/helloworld.nim $(WASM32_NIMC) --out:examples/ee/block_echo.wasm examples/ee/block_echo.nim - .PHONY: test-ee test-ee: ee-examples cd tests/ee/; \ ./test.sh + +.PHONY: substrate-examples +substrate-examples: + $(WASM32_NIMC) --out:examples/substrate/hello_world.wasm examples/substrate/hello_world.nim + +.PHONY: test-substrate +test-substrate: substrate-examples + cd tests/substrate; \ + SUBSTRATE_PATH="${HOME}/.cargo/bin/substrate" ./test.sh diff --git a/nimplay/substrate_runtime.nim b/nimplay/substrate_runtime.nim index fb17b42..72309b3 100644 --- a/nimplay/substrate_runtime.nim +++ b/nimplay/substrate_runtime.nim @@ -1,15 +1,21 @@ -import utils +import macros {.push cdecl, importc.} +proc ext_address*() proc ext_block_number*() +proc ext_gas_left*() +proc ext_gas_price*() proc ext_get_storage*(key_ptr: pointer): int32 -proc ext_println*(str_ptr: pointer, str_len: int32) +proc ext_now*() +proc ext_println*(str_ptr: pointer, str_len: int32) # experimental; will be removed. +proc ext_random_seed*() proc ext_scratch_read*(dest_ptr: pointer, offset: int32, len: int32) proc ext_scratch_size*(): int32 -proc ext_scratch_write(src_ptr: pointer, len: int32) +proc ext_scratch_write*(src_ptr: pointer, len: int32) proc ext_set_rent_allowance*(value_ptr: pointer, value_len: int32) proc ext_set_storage*(key_ptr: pointer, value_non_null: int32, value_ptr: int32, value_len: int32) +proc ext_value_transferred*() {.pop.} diff --git a/tests/substrate/test.sh b/tests/substrate/test.sh new file mode 100755 index 0000000..dee896c --- /dev/null +++ b/tests/substrate/test.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -z "$SUBSTRATE_PATH" ]; then + echo "Please specify the path to substrate in the SUBSTRATE_PATH environment variable" + exit 1 +fi + +if [ ! -f "$SUBSTRATE_PATH" ]; then + echo "$SUBSTRATE_PATH doesn't exist" + exit 2 +fi + +# Purge dev chain and then spin up the substrate node in background +$SUBSTRATE_PATH purge-chain --dev -y +$SUBSTRATE_PATH --dev & +SUBSTRATE_PID=$! + +# # Execute tests +yarn && yarn test --verbose + +# # Kill the spawned substrate node +kill -9 $SUBSTRATE_PID diff --git a/tests/substrate/tests/consts.ts b/tests/substrate/tests/consts.ts new file mode 100644 index 0000000..6a6ef86 --- /dev/null +++ b/tests/substrate/tests/consts.ts @@ -0,0 +1,9 @@ +import BN from "bn.js"; + +export const WSURL = "ws://127.0.0.1:9944"; +export const DOT: BN = new BN("1000000000000000"); +export const CREATION_FEE: BN = DOT.muln(200); +export const GAS_REQUIRED = 50000; +export const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; +export const BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"; + diff --git a/tests/substrate/tests/contract-nimplay.spec.ts b/tests/substrate/tests/contract-nimplay.spec.ts new file mode 100644 index 0000000..727028d --- /dev/null +++ b/tests/substrate/tests/contract-nimplay.spec.ts @@ -0,0 +1,98 @@ +// Adapted from https://github.com/paritytech/srml-contracts-waterfall/ + +import { ApiPromise, SubmittableResult, WsProvider } from "@polkadot/api"; +import { Abi } from '@polkadot/api-contract'; +import testKeyring from "@polkadot/keyring/testing"; +import { u8aToHex } from "@polkadot/util"; +import { randomAsU8a } from "@polkadot/util-crypto"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { Option } from "@polkadot/types"; +import { Address, ContractInfo, Hash } from "@polkadot/types/interfaces"; + +import { ALICE, CREATION_FEE, WSURL } from "./consts"; +import { + callContract, + instantiate, + getContractStorage, + putCode +} from "./utils"; + +// This is a test account that is going to be created and funded each test. +const keyring = testKeyring({ type: "sr25519" }); +const alicePair = keyring.getPair(ALICE); +let testAccount: KeyringPair; +let api: ApiPromise; + +beforeAll((): void => { + jest.setTimeout(30000); +}); + +beforeEach( + async (done): Promise<() => void> => { + api = await ApiPromise.create({ provider: new WsProvider(WSURL) }); + testAccount = keyring.addFromSeed(randomAsU8a(32)); + + return api.tx.balances + .transfer(testAccount.address, CREATION_FEE.muln(3)) + .signAndSend(alicePair, (result: SubmittableResult): void => { + if ( + result.status.isFinalized && + result.findRecord("system", "ExtrinsicSuccess") + ) { + console.log("New test account has been created."); + done(); + } + }); + } +); + + + +describe("Nimplay Hello World", () => { + test("Raw Flipper contract", async (done): Promise => { + // See https://github.com/paritytech/srml-contracts-waterfall/issues/6 for info about + // how to get the STORAGE_KEY of an instantiated contract + + const STORAGE_KEY = (new Uint8Array(32)).fill(2); + // Deploy contract code on chain and retrieve the code hash + const codeHash = await putCode( + api, + testAccount, + "../../../examples/substrate/hello_world.wasm" + ); + expect(codeHash).toBeDefined(); + + // Instantiate a new contract instance and retrieve the contracts address + // Call contract with Action: 0x00 = Action::Flip() + const address: Address = await instantiate( + api, + testAccount, + codeHash, + "0x00", + CREATION_FEE + ); + expect(address).toBeDefined(); + + const initialValue: Uint8Array = await getContractStorage( + api, + address, + STORAGE_KEY + ); + expect(initialValue).toBeDefined(); + expect(initialValue.toString()).toEqual("0x00"); + + await callContract(api, testAccount, address, "0x00"); + + const newValue = await getContractStorage(api, address, STORAGE_KEY); + expect(newValue.toString()).toEqual("0x01"); + + await callContract(api, testAccount, address, "0x00"); + + const flipBack = await getContractStorage(api, address, STORAGE_KEY); + expect(flipBack.toString()).toEqual("0x00"); + + done(); + }); + + +}); diff --git a/tests/substrate/tests/utils.ts b/tests/substrate/tests/utils.ts new file mode 100644 index 0000000..0119fd6 --- /dev/null +++ b/tests/substrate/tests/utils.ts @@ -0,0 +1,109 @@ +import { ApiPromise, SubmittableResult } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { Option, StorageData } from "@polkadot/types"; +import { Address, ContractInfo, Hash } from "@polkadot/types/interfaces"; +import BN from "bn.js"; +import fs from "fs"; +import path from "path"; +const blake = require('blakejs') + +import { GAS_REQUIRED } from "./consts"; + +export async function sendAndReturnFinalized(signer: KeyringPair, tx: any) { + return new Promise(function(resolve, reject) { + tx.signAndSend(signer, (result: SubmittableResult) => { + if (result.status.isFinalized) { + // Return result of the submittable extrinsic after the transfer is finalized + resolve(result as SubmittableResult); + } + if ( + result.status.isDropped || + result.status.isInvalid || + result.status.isUsurped + ) { + reject(result as SubmittableResult); + console.error("ERROR: Transaction could not be finalized."); + } + }); + }); +} + +export async function putCode( + api: ApiPromise, + signer: KeyringPair, + fileName: string, + gasRequired: number = GAS_REQUIRED +): Promise { + const wasmCode = fs + .readFileSync(path.join(__dirname, fileName)) + .toString("hex"); + const tx = api.tx.contracts.putCode(gasRequired, `0x${wasmCode}`); + const result: any = await sendAndReturnFinalized(signer, tx); + console.log('result', result) + const record = result.findRecord("contracts", "CodeStored"); + + if (!record) { + console.error("ERROR: No code stored after executing putCode()"); + } + // Return code hash. + console.log(record); + return record.event.data[0]; +} + +export async function instantiate( + api: ApiPromise, + signer: KeyringPair, + codeHash: Hash, + inputData: any, + endowment: BN, + gasRequired: number = GAS_REQUIRED +): Promise
{ + const tx = api.tx.contracts.instantiate( + endowment, + gasRequired, + codeHash, + inputData + ); + const result: any = await sendAndReturnFinalized(signer, tx); + const record = result.findRecord("contracts", "Instantiated"); + + if (!record) { + console.error("ERROR: No new instantiated contract"); + } + // Return the address of instantiated contract. + return record.event.data[1]; +} + +export async function callContract( + api: ApiPromise, + signer: KeyringPair, + contractAddress: Address, + inputData: any, + gasRequired: number = GAS_REQUIRED, + endowment: number = 0 +): Promise { + const tx = api.tx.contracts.call( + contractAddress, + endowment, + gasRequired, + inputData + ); + await sendAndReturnFinalized(signer, tx); +} + +export async function getContractStorage( + api: ApiPromise, + contractAddress: Address, + storageKey: Uint8Array +): Promise { + const contractInfo = await api.query.contracts.contractInfoOf( + contractAddress + ); + // Return the value of the contracts storage + const storageKeyBlake2b = blake.blake2bHex(storageKey, null, 32); + return await api.rpc.state.getChildStorage( + (contractInfo as Option).unwrap().asAlive.trieId, + '0x' + storageKeyBlake2b + ); +} + diff --git a/tests/substrate/tsconfig.js b/tests/substrate/tsconfig.js new file mode 100644 index 0000000..14b7763 --- /dev/null +++ b/tests/substrate/tsconfig.js @@ -0,0 +1,24 @@ +{ + + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "lib": ["dom", "es2018"], + "resolveJsonModule": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "allowJs": true, + "esModuleInterop": true + }, + "exclude": [ + "contracts/rust", + "lib", + "node_modules", + ] +} diff --git a/tests/substrate/utils.ts b/tests/substrate/utils.ts new file mode 100644 index 0000000..6a6ef86 --- /dev/null +++ b/tests/substrate/utils.ts @@ -0,0 +1,9 @@ +import BN from "bn.js"; + +export const WSURL = "ws://127.0.0.1:9944"; +export const DOT: BN = new BN("1000000000000000"); +export const CREATION_FEE: BN = DOT.muln(200); +export const GAS_REQUIRED = 50000; +export const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; +export const BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"; +