Эх сурвалжийг харах

Add some substrate integration tests

Signed-off-by: Sean Young <sean@mess.org>
Sean Young 4 жил өмнө
parent
commit
ff49190b3c
38 өөрчлөгдсөн 1617 нэмэгдсэн , 106 устгасан
  1. 33 4
      .github/workflows/test.yml
  2. 7 4
      docs/targets.rst
  3. 4 0
      integration/substrate/.gitignore
  4. 73 0
      integration/substrate/arrays.sol
  5. 78 0
      integration/substrate/arrays.spec.ts
  6. 13 0
      integration/substrate/asserts.sol
  7. 46 0
      integration/substrate/asserts.spec.ts
  8. 18 0
      integration/substrate/builtins.sol
  9. 40 0
      integration/substrate/builtins.spec.ts
  10. 22 0
      integration/substrate/create_contract.sol
  11. 52 0
      integration/substrate/create_contract.spec.ts
  12. 23 0
      integration/substrate/events.sol
  13. 44 0
      integration/substrate/events.spec.ts
  14. 60 0
      integration/substrate/external_call.sol
  15. 67 0
      integration/substrate/external_call.spec.ts
  16. 20 0
      integration/substrate/flipper.sol
  17. 30 0
      integration/substrate/flipper.spec.ts
  18. 69 0
      integration/substrate/index.ts
  19. 29 0
      integration/substrate/package.json
  20. 148 0
      integration/substrate/primitives.sol
  21. 127 0
      integration/substrate/primitives.spec.ts
  22. 66 0
      integration/substrate/store.sol
  23. 113 0
      integration/substrate/store.spec.ts
  24. 100 0
      integration/substrate/structs.sol
  25. 137 0
      integration/substrate/structs.spec.ts
  26. 4 0
      integration/substrate/to-do
  27. 62 0
      integration/substrate/tsconfig.json
  28. 14 11
      src/abi/substrate.rs
  29. 1 1
      src/codegen/cfg.rs
  30. 2 2
      src/codegen/storage.rs
  31. 6 2
      src/emit/mod.rs
  32. 23 10
      src/emit/substrate.rs
  33. 58 17
      tests/substrate.rs
  34. 2 2
      tests/substrate_tests/arrays.rs
  35. 2 2
      tests/substrate_tests/builtins.rs
  36. 20 34
      tests/substrate_tests/calls.rs
  37. 3 16
      tests/substrate_tests/contracts.rs
  38. 1 1
      tests/substrate_tests/value.rs

+ 33 - 4
.github/workflows/test.yml

@@ -127,7 +127,7 @@ jobs:
       uses: actions/checkout@v2
     - uses: actions/setup-node@v1
       with:
-        node-version: '12'
+        node-version: '14'
     - uses: actions/download-artifact@master
       with:
         name: solang-linux
@@ -136,15 +136,44 @@ jobs:
     - run: echo "$(pwd)/bin" >> $GITHUB_PATH
     - run: npm install
       working-directory: ./integration/solana
-    - name: Build Solang contract
+    - name: Build Solang contracts
       run: npm run build
       working-directory: ./integration/solana
     - name: Set github env
       run: echo "RPC_URL=http://solana:8899/" >> $GITHUB_ENV
-    - name: Deploy and test contract
+    - name: Deploy and test contracts
       run: npm run test
       working-directory: ./integration/solana
 
+  substrate:
+    name: Substrate Integration test
+    runs-on: ubuntu-18.04
+    needs: linux
+    steps:
+    - name: Checkout sources
+      uses: actions/checkout@v2
+      # We can't run substrate as a github actions service, since it requires
+      # command line arguments. See https://github.com/actions/runner/pull/1152
+    - name: Start substrate
+      run: docker run -d -p 9944:9944 parity/substrate:latest --dev --ws-external
+    - uses: actions/setup-node@v1
+      with:
+        node-version: '14'
+    - uses: actions/download-artifact@master
+      with:
+        name: solang-linux
+        path: bin
+    - run: chmod 755 ./bin/solang
+    - run: echo "$(pwd)/bin" >> $GITHUB_PATH
+    - run: npm install
+      working-directory: ./integration/substrate
+    - name: Build Solang contracts
+      run: npm run build
+      working-directory: ./integration/substrate
+    - name: Deploy and test contracts
+      run: npm run test
+      working-directory: ./integration/substrate
+
   burrow:
     name: Burrow Integration test
     runs-on: ubuntu-18.04
@@ -160,7 +189,7 @@ jobs:
       uses: actions/checkout@v2
     - uses: actions/setup-node@v1
       with:
-        node-version: '12'
+        node-version: '14'
     - uses: actions/download-artifact@master
       with:
         name: solang-linux

+ 7 - 4
docs/targets.rst

@@ -5,14 +5,17 @@ Target Specific
 Parity Substrate
 ________________
 
-Solang works with Parity Substrate 3.0. This target is the most mature and has received the most testing so far.
+Solang works with Parity Substrate 2.0 or later. This target is the most mature and has received the most testing so far.
 
 The Parity Substrate has the following differences to Ethereum Solidity:
 
-- The address type is 32 bytes, not 20 bytes
+- The address type is 32 bytes, not 20 bytes. This is what Substrate calls an "account"
 - An address literal has to be specified using the ``address"5GBWmgdFAMqm8ZgAHGobqDqX6tjLxJhv53ygjNtaaAn3sjeZ"`` syntax
-- ABI encoding and decoding is done using the `SCALE <https://substrate.dev/docs/en/overview/low-level-data-format>`_ encoding
+- ABI encoding and decoding is done using the `SCALE <https://substrate.dev/docs/en/knowledgebase/advanced/codec>`_ encoding
 - Multiple constructors are allowed, and can be overloaded
+- There is no ``ecrecover()`` builtin function, or any other function to recover or verify cryptographic signatures at runtime
+- Only functions called via rpc may return values; when calling a function in a transaction, the return values cannot be accessed
+- An `assert()`, `require()`, or `revert()` executes the wasm unreachable instruction. The reason code is lost
 
 There is an solidity example which can be found in the
 `examples <https://github.com/hyperledger-labs/solang/tree/main/examples>`_
@@ -24,7 +27,7 @@ directory. Write this to flipper.sol and run:
 
 Now you should have a file called ``flipper.contract``. The file contains both the ABI and contract wasm.
 It can be used directly in the
-`Polkadot UI <https://substrate.dev/substrate-contracts-workshop/#/0/deploying-your-contract?id=putting-your-code-on-the-blockchain>`_, as if the contract was written in ink!.
+`Polkadot UI <https://substrate.dev/substrate-contracts-workshop/#/0/deploy-contract>`_, as if the contract was written in ink!.
 
 
 Solana

+ 4 - 0
integration/substrate/.gitignore

@@ -0,0 +1,4 @@
+*.js
+*.contract
+node_modules
+package-lock.json

+ 73 - 0
integration/substrate/arrays.sol

@@ -0,0 +1,73 @@
+enum Permission {
+	Perm1, Perm2, Perm3, Perm4, Perm5, Perm6, Perm7, Perm8
+}
+
+/// This is a test contract which tests arrays
+contract arrays {
+	uint64 user_count;
+
+	struct user {
+		string name;
+		bytes32 addr;
+		uint64 id;
+		Permission[] perms;
+	}
+
+	// declare a sparse array. Sparse arrays are arrays which are too large to
+	// fit into account data on Solana; this is not neccessarily a Solidity feature
+	user[type(uint64).max] users;
+
+	mapping (bytes32 => uint64) addressToUser;
+
+	function addUser(uint64 id, bytes32 addr, string name, Permission[] perms) public {
+		user storage u = users[id];
+
+		u.name = name;
+		u.addr = addr;
+		u.id = id;
+		u.perms = perms;
+
+		addressToUser[addr] = id;
+
+		assert(id <= users.length);
+	}
+
+	function getUserById(uint64 id) public view returns (user) {
+		assert(users[id].id == id);
+
+		return users[id];
+	}
+
+	function getUserByAddress(bytes32 addr) public view returns (user) {
+		uint64 id = addressToUser[addr];
+
+		assert(users[id].id == id);
+
+		return users[id];
+	}
+
+	function userExists(uint64 id) public view returns (bool) {
+		return users[id].id == id;
+	}
+
+	function removeUser(uint64 id) public {
+		bytes32 addr = users[id].addr;
+
+		delete users[id];
+		delete addressToUser[addr];
+	}
+
+	function hasPermission(uint64 id, Permission p) public view returns (bool) {
+		user storage u = users[id];
+
+		assert(u.id == id);
+
+		for (uint32 i = 0; i < u.perms.length; i++) {
+			if (u.perms[i] == p) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+}

+ 78 - 0
integration/substrate/arrays.spec.ts

@@ -0,0 +1,78 @@
+import expect from 'expect';
+import crypto from 'crypto';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+
+describe('Deploy arrays contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('arrays in account storage', async function () {
+        this.timeout(50000);
+
+        const alice = aliceKeypair();
+
+        let deployed_contract = await deploy(conn, alice, 'arrays.contract');
+
+        let contract = new ContractPromise(conn, deployed_contract.abi, deployed_contract.address);
+
+        let users = [];
+
+        for (let i = 0; i < 3; i++) {
+            let addr = '0x' + crypto.randomBytes(32).toString('hex');
+            let name = `name${i}`;
+            let id = crypto.randomBytes(4).readUInt32BE(0);
+            let perms: string[] = [];
+
+            for (let j = 0; j < Math.random() * 3; j++) {
+                let p = Math.floor(Math.random() * 8);
+
+                perms.push(`Perm${p + 1}`);
+            }
+
+            const tx1 = contract.tx.addUser({ gasLimit }, id, addr, name, perms);
+
+            await transaction(tx1, alice);
+
+            users.push({ "name": name, "addr": addr, "id": id, "perms": perms });
+        }
+
+        let user = users[Math.floor(Math.random() * users.length)];
+
+        let res1 = await contract.query.getUserById(alice.address, {}, user.id);
+
+        expect(res1.output?.toJSON()).toStrictEqual(user);
+
+        if (user.perms.length > 0) {
+            let perms = user.perms;
+
+            let p = perms[Math.floor(Math.random() * perms.length)];
+
+            let res2 = await contract.query.hasPermission(alice.address, {}, user.id, p);
+
+            expect(res2.output?.toJSON()).toBe(true);
+        }
+
+        user = users[Math.floor(Math.random() * users.length)];
+
+        let res3 = await contract.query.getUserByAddress(alice.address, {}, user.addr);
+
+        expect(res3.output?.toJSON()).toStrictEqual(user);
+
+        const tx2 = contract.tx.removeUser({ gasLimit }, user.id);
+
+        await transaction(tx2, alice);
+
+        let res4 = await contract.query.userExists(alice.address, {}, user.id);
+
+        expect(res4.output?.toJSON()).toBe(false);
+    });
+});

+ 13 - 0
integration/substrate/asserts.sol

@@ -0,0 +1,13 @@
+
+contract asserts {
+	int64 public var = 1;
+
+	function test_assert() public {
+		var = 2;
+		assert(false);
+	}
+
+	function test_assert_rpc() public pure returns (int64) {
+		revert("I refuse");
+	}
+}

+ 46 - 0
integration/substrate/asserts.spec.ts

@@ -0,0 +1,46 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+
+describe('Deploy asserts contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('asserts', async function () {
+        this.timeout(50000);
+
+        const alice = aliceKeypair();
+
+        // call the constructors
+        let deploy_contract = await deploy(conn, alice, 'asserts.contract');
+
+        let contract = new ContractPromise(conn, deploy_contract.abi, deploy_contract.address);
+
+        let res0 = await contract.query.var(alice.address, {});
+
+        expect(res0.output?.toJSON()).toEqual(1);
+
+        let res1 = await contract.query.testAssertRpc(alice.address, {});
+        expect(res1.result.toHuman()).toEqual({ "Err": { "Module": { "error": "19", "index": "18", "message": "ContractTrapped" } } });
+
+        let tx = contract.tx.testAssert({ gasLimit });
+
+        let res2 = await transaction(tx, alice).then(() => {
+            throw new Error("should not succeed");
+        }, (res) => res);
+
+        expect(res2.dispatchError.toHuman()).toEqual({ "Module": { "error": "19", "index": "18" } });
+
+        let res3 = await contract.query.var(alice.address, {});
+
+        expect(res3.output?.toJSON()).toEqual(1);
+    });
+});

+ 18 - 0
integration/substrate/builtins.sol

@@ -0,0 +1,18 @@
+
+contract builtins {
+	function hash_ripemd160(bytes memory bs) public pure returns (bytes20) {
+		return ripemd160(bs);
+	}
+
+	function hash_kecccak256(bytes memory bs) public pure returns (bytes32) {
+		return keccak256(bs);
+	}
+
+	function hash_sha256(bytes memory bs) public pure returns (bytes32) {
+		return sha256(bs);
+	}
+
+	function mr_now() public view returns (uint64) {
+		return block.timestamp;
+	}
+}

+ 40 - 0
integration/substrate/builtins.spec.ts

@@ -0,0 +1,40 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+
+describe('Deploy builtin contract and test', () => {
+    it('builtins', async function () {
+        this.timeout(50000);
+
+        let conn = await createConnection();
+        const alice = aliceKeypair();
+
+        let deployed_contract = await deploy(conn, alice, 'builtins.contract');
+
+        let contract = new ContractPromise(conn, deployed_contract.abi, deployed_contract.address);
+
+        // call the constructor
+        let ripemd160_res = await contract.query.hashRipemd160(alice.address, {}, '0x' + Buffer.from('Call me Ishmael.', 'utf8').toString('hex'));
+
+        expect(ripemd160_res.output?.toJSON()).toBe("0x0c8b641c461e3c7abbdabd7f12a8905ee480dadf");
+
+        let sha256_res = await contract.query.hashSha256(alice.address, {}, '0x' + Buffer.from('Call me Ishmael.', 'utf8').toString('hex'));
+
+        expect(sha256_res.output?.toJSON()).toBe("0x458f3ceeeec730139693560ecf66c9c22d9c7bc7dcb0599e8e10b667dfeac043");
+
+        let keccak256_res = await contract.query.hashKecccak256(alice.address, {}, '0x' + Buffer.from('Call me Ishmael.', 'utf8').toString('hex'));
+
+        expect(keccak256_res.output?.toJSON()).toBe("0x823ad8e1757b879aac338f9a18542928c668e479b37e4a56f024016215c5928c");
+
+        let timestamps_res = await contract.query.mrNow(alice.address, {});
+
+        let now = Math.floor(+new Date() / 1000);
+
+        let ts = Number(timestamps_res.output?.toJSON());
+
+        expect(ts).toBeLessThanOrEqual(now);
+        expect(ts).toBeGreaterThan(now - 120);
+
+        conn.disconnect();
+    });
+});

+ 22 - 0
integration/substrate/create_contract.sol

@@ -0,0 +1,22 @@
+
+contract creator {
+    child public c;
+
+    function child_as_u256() public view returns (uint) {
+        return uint(address(c));
+    }
+
+    function create_child() public {
+        c = new child();
+    }
+
+    function call_child() public pure returns (string) {
+        return c.say_my_name();
+    }
+}
+
+contract child {
+    function say_my_name() pure public returns (string) {
+        return "child";
+    }
+}

+ 52 - 0
integration/substrate/create_contract.spec.ts

@@ -0,0 +1,52 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+
+describe('Deploy create_contract contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('create_contract', async function () {
+        this.timeout(50000);
+
+        const alice = aliceKeypair();
+
+        // call the constructors
+        let deploy_contract = await deploy(conn, alice, 'creator.contract');
+
+        // we need to have upload the child code
+        let _ = await deploy(conn, alice, 'child.contract');
+
+        let contract = new ContractPromise(conn, deploy_contract.abi, deploy_contract.address);
+
+        let res0 = await contract.query.childAsU256(alice.address, {});
+
+        console.log("child: " + res0.output?.toString());
+
+        let tx = contract.tx.createChild({ gasLimit });
+
+        await transaction(tx, alice).then(null,
+            (res) => {
+                console.log("nah: " + JSON.stringify(res));
+            }
+        );
+
+        let res1 = await contract.query.childAsU256(alice.address, {});
+
+        let child_adress = res1.output?.toString();
+
+        console.log("child: " + child_adress);
+
+        // let res2 = await contract.query.callChild(alice.address, {});
+
+        // expect(res2.output?.toJSON()).toStrictEqual("child");
+    });
+});

+ 23 - 0
integration/substrate/events.sol

@@ -0,0 +1,23 @@
+contract events {
+	/// Ladida tada
+	event foo1(
+		int64 id,
+		string s
+	);
+
+	/// @title Event Foo2
+	/// @notice Just a test
+	/// @author them is me
+	event foo2(
+		int64 id,
+		string s2,
+		address a
+	);
+
+
+	function emit_event() public {
+		emit foo1(254, "hello there");
+
+		emit foo2(type(int64).max, "minor", address(this));
+	}
+}

+ 44 - 0
integration/substrate/events.spec.ts

@@ -0,0 +1,44 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+import { DecodedEvent } from '@polkadot/api-contract/types';
+
+describe('Deploy events contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('events', async function () {
+        this.timeout(50000);
+
+        const alice = aliceKeypair();
+
+        // call the constructors
+        let deploy_contract = await deploy(conn, alice, 'events.contract');
+
+        let contract = new ContractPromise(conn, deploy_contract.abi, deploy_contract.address);
+
+        let tx = contract.tx.emitEvent({ gasLimit });
+
+        let res0: any = await transaction(tx, alice);
+
+        let events: DecodedEvent[] = res0.contractEvents;
+
+        expect(events.length).toEqual(2);
+
+        expect(events[0].event.identifier).toBe("foo1");
+        expect(events[0].event.docs).toEqual(["Ladida tada\n\n"]);
+        expect(events[0].args.map(a => a.toJSON())).toEqual([254, "hello there"]);
+
+        expect(events[1].event.identifier).toBe("foo2");
+        expect(events[1].event.docs).toEqual(["Event Foo2\n\nJust a test\n\nAuthor: them is me"]);
+        expect(events[1].args.map(a => a.toJSON())).toEqual(["0x7fffffffffffffff", "minor", deploy_contract.address.toString()]);
+    });
+});

+ 60 - 0
integration/substrate/external_call.sol

@@ -0,0 +1,60 @@
+
+contract caller {
+    function do_call(callee e, int64 v) public {
+        e.set_x(v);
+    }
+
+    function do_call2(callee e, int64 v) view public returns (int64) {
+        return v + e.get_x();
+    }
+
+    // call two different functions
+    function do_call3(callee e, callee2 e2, int64[4] memory x, string memory y) pure public returns (int64, string memory) {
+        return (e2.do_stuff(x), e.get_name());
+    }
+
+    // call two different functions
+    function do_call4(callee e, callee2 e2, int64[4] memory x, string memory y) pure public returns (int64, string memory) {
+        return (e2.do_stuff(x), e.call2(e2, y));
+    }
+
+    function who_am_i() public view returns (address) {
+        return address(this);
+    }
+}
+
+contract callee {
+    int64 x;
+
+    function set_x(int64 v) public {
+        x = v;
+    }
+
+    function get_x() public view returns (int64) {
+        return x;
+    }
+
+    function call2(callee2 e2, string s) public pure returns (string) {
+        return e2.do_stuff2(s);
+    }
+
+    function get_name() public pure returns (string) {
+        return "my name is callee";
+    }
+}
+
+contract callee2 {
+    function do_stuff(int64[4] memory x) public pure returns (int64) {
+        int64 total = 0;
+
+        for (uint i=0; i< x.length; i++)  {
+            total += x[i];
+        }
+
+        return total;
+    }
+
+    function do_stuff2(string x) public pure returns (string) {
+        return "x:" + x;
+    }
+}

+ 67 - 0
integration/substrate/external_call.spec.ts

@@ -0,0 +1,67 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+
+describe('Deploy external_call contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('external_call', async function () {
+        this.timeout(100000);
+
+        const alice = aliceKeypair();
+
+        // call the constructors
+        let caller_res = await deploy(conn, alice, 'caller.contract');
+
+        let caller_contract = new ContractPromise(conn, caller_res.abi, caller_res.address);
+
+        let callee_res = await deploy(conn, alice, 'callee.contract');
+
+        let callee_contract = new ContractPromise(conn, callee_res.abi, callee_res.address);
+
+        let callee2_res = await deploy(conn, alice, 'callee2.contract');
+
+        let callee2_contract = new ContractPromise(conn, callee2_res.abi, callee2_res.address);
+
+        let tx1 = callee_contract.tx.setX({ gasLimit }, 102);
+
+        await transaction(tx1, alice);
+
+        let res1 = await callee_contract.query.getX(alice.address, {});
+
+        expect(res1.output?.toJSON()).toStrictEqual(102);
+
+        let res2 = await caller_contract.query.whoAmI(alice.address, {});
+
+        expect(res2.output?.toString()).toEqual(caller_res.address.toString());
+
+        let tx2 = caller_contract.tx.doCall({ gasLimit }, callee_contract.address, 13123);
+
+        await transaction(tx2, alice);
+
+        let res3 = await callee_contract.query.getX(alice.address, {});
+
+        expect(res3.output?.toJSON()).toStrictEqual(13123);
+
+        let res4 = await caller_contract.query.doCall2(alice.address, {}, callee_contract.address, 20000);
+
+        expect(res4.output?.toJSON()).toStrictEqual(33123);
+
+        let res5 = await caller_contract.query.doCall3(alice.address, {}, callee_contract.address, callee2_contract.address, [3, 5, 7, 9], "yo");
+
+        expect(res5.output?.toJSON()).toEqual([24, "my name is callee"]);
+
+        let res6 = await caller_contract.query.doCall4(alice.address, {}, callee_contract.address, callee2_contract.address, [1, 2, 3, 4], "asda");
+
+        expect(res6.output?.toJSON()).toEqual([10, "x:asda"]);
+    });
+});

+ 20 - 0
integration/substrate/flipper.sol

@@ -0,0 +1,20 @@
+contract flipper {
+	bool private value;
+
+	/// Constructor that initializes the `bool` value to the given `init_value`.
+	constructor(bool initvalue) {
+		value = initvalue;
+	}
+
+	/// A message that can be called on instantiated contracts.
+	/// This one flips the value of the stored `bool` from `true`
+	/// to `false` and vice versa.
+	function flip() public {
+		value = !value;
+	}
+
+	/// Simply returns the current value of our `bool`.
+	function get() public view returns (bool) {
+		return value;
+	}
+}

+ 30 - 0
integration/substrate/flipper.spec.ts

@@ -0,0 +1,30 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+
+describe('Deploy flipper contract and test', () => {
+    it('flipper', async function () {
+        this.timeout(50000);
+
+        let conn = await createConnection();
+        const alice = aliceKeypair();
+
+        let deployed_contract = await deploy(conn, alice, 'flipper.contract', true);
+
+        let contract = new ContractPromise(conn, deployed_contract.abi, deployed_contract.address);
+
+        let init_value = await contract.query.get(alice.address, {});
+
+        expect(init_value.output?.toJSON()).toBe(true);
+
+        const tx = contract.tx.flip({ gasLimit });
+
+        await transaction(tx, alice);
+
+        let flipped_value = await contract.query.get(alice.address, {});
+
+        expect(flipped_value.output?.toJSON()).toBe(false);
+
+        conn.disconnect();
+    });
+});

+ 69 - 0
integration/substrate/index.ts

@@ -0,0 +1,69 @@
+import fs, { PathLike } from 'fs';
+import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
+import { CodePromise, ContractPromise } from '@polkadot/api-contract';
+import { SubmittableExtrinsic } from '@polkadot/api/types';
+import { ISubmittableResult } from '@polkadot/types/types';
+import { KeyringPair } from '@polkadot/keyring/types';
+
+const default_url: string = "ws://localhost:9944";
+export const gasLimit: BigInt = 200000n * 1000000n;
+
+export function aliceKeypair(): KeyringPair {
+  const keyring = new Keyring({ type: 'sr25519' });
+  return keyring.addFromUri('//Alice');
+}
+
+export async function createConnection(): Promise<ApiPromise> {
+  let url = process.env.RPC_URL || default_url;
+
+  return ApiPromise.create({ provider: new WsProvider(url) });
+}
+
+export async function deploy(api: ApiPromise, pair: KeyringPair, file: PathLike, ...params: any[]): Promise<any> {
+  const contractJson = fs.readFileSync(file, { encoding: 'utf-8' });
+
+  const code = new CodePromise(api, contractJson, null);
+
+  const tx = code.tx.new({ gasLimit, value: 1000_000_000_000_000_000n }, ...params);
+
+  return new Promise(async (resolve, reject) => {
+    const unsub = await tx.signAndSend(pair, (result: any) => {
+      if (result.status.isInBlock || result.status.isFinalized) {
+        resolve(result.contract);
+        unsub();
+      }
+
+      if (result.isError) {
+        if (result.dispatchError) {
+          console.log(result.dispatchError.toHuman());
+        } else {
+          console.log(result.asError.toHuman());
+        }
+
+        reject(result);
+        unsub();
+      }
+    });
+  });
+}
+
+export async function transaction(tx: SubmittableExtrinsic<"promise", ISubmittableResult>, pair: KeyringPair): Promise<ISubmittableResult> {
+  return new Promise(async (resolve, reject) => {
+    const unsub = await tx.signAndSend(pair, (result: any) => {
+      if (result.dispatchError) {
+        reject(result);
+        unsub();
+      }
+
+      if (result.isError) {
+        reject(result);
+        unsub();
+      }
+
+      if (result.status.isInBlock || result.status.isFinalized) {
+        resolve(result);
+        unsub();
+      }
+    });
+  });
+}

+ 29 - 0
integration/substrate/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "substrate-tests",
+  "version": "0.0.1",
+  "description": "Integration tests with Solang and Substrate",
+  "main": "index.js",
+  "scripts": {
+    "test": "tsc; ts-mocha *.spec.ts",
+    "build": "solang *.sol --target substrate -v"
+  },
+  "author": "Sean Young <sean@mess.org>",
+  "license": "Apache-2.0",
+  "devDependencies": {
+    "@types/mocha": "^8.0.4",
+    "@types/node": "^14.14.10",
+    "expect": "^26.6.2",
+    "mocha": "^8.2.1",
+    "ts-mocha": "^8.0.0",
+    "jest": "^24.9.0",
+    "ts-jest": "^24.1.0",
+    "typescript": "^4.1.2"
+  },
+  "dependencies": {
+    "@polkadot/api": "^4.13",
+    "@polkadot/api-contract": "^4.13",
+    "@polkadot/types": "^4.13",
+    "@polkadot/keyring": "^6.7",
+    "@polkadot/util-crypto": "^6.7"
+  }
+}

+ 148 - 0
integration/substrate/primitives.sol

@@ -0,0 +1,148 @@
+
+contract primitives {
+	enum oper { add, sub, mul, div, mod, pow, shl, shr, or, and, xor }
+
+	function is_mul(oper op) pure public returns (bool) {
+		return op == oper.mul;
+	}
+
+	function return_div() pure public returns (oper) {
+		return oper.div;
+	}
+
+	function op_i64(oper op, int64 a, int64 b) pure public returns (int64) {
+		if (op == oper.add) {
+			return a + b;
+		} else if (op == oper.sub) {
+			return a - b;
+		} else if (op == oper.mul) {
+			return a * b;
+		} else if (op == oper.div) {
+			return a / b;
+		} else if (op == oper.mod) {
+			return a % b;
+		} else if (op == oper.shl) {
+			return a << b;
+		} else if (op == oper.shr) {
+			return a >> b;
+		} else {
+			revert();
+		}
+	}
+
+	function op_u64(oper op, uint64 a, uint64 b) pure public returns (uint64) {
+		if (op == oper.add) {
+			return a + b;
+		} else if (op == oper.sub) {
+			return a - b;
+		} else if (op == oper.mul) {
+			return a * b;
+		} else if (op == oper.div) {
+			return a / b;
+		} else if (op == oper.mod) {
+			return a % b;
+		} else if (op == oper.pow) {
+			return a ** b;
+		} else if (op == oper.shl) {
+			return a << b;
+		} else if (op == oper.shr) {
+			return a >> b;
+		} else {
+			revert();
+		}
+	}
+
+	function op_u256(oper op, uint256 a, uint256 b) pure public returns (uint256) {
+		if (op == oper.add) {
+			return a + b;
+		} else if (op == oper.sub) {
+			return a - b;
+		} else if (op == oper.mul) {
+			return a * b;
+		} else if (op == oper.div) {
+			return a / b;
+		} else if (op == oper.mod) {
+			return a % b;
+		} else if (op == oper.pow) {
+			return a ** uint256(b);
+		} else if (op == oper.shl) {
+			return a << b;
+		} else if (op == oper.shr) {
+			return a >> b;
+		} else {
+			revert();
+		}
+	}
+
+	function op_i256(oper op, int256 a, int256 b) pure public returns (int256) {
+		if (op == oper.add) {
+			return a + b;
+		} else if (op == oper.sub) {
+			return a - b;
+		} else if (op == oper.mul) {
+			return a * b;
+		} else if (op == oper.div) {
+			return a / b;
+		} else if (op == oper.mod) {
+			return a % b;
+		} else if (op == oper.shl) {
+			return a << b;
+		} else if (op == oper.shr) {
+			return a >> b;
+		} else {
+			revert();
+		}
+	}
+
+	function return_u8_6() public pure returns (bytes6) {
+		return "ABCDEF";
+	}
+
+	function op_u8_5_shift(oper op, bytes5 a, uint64 r) pure public returns (bytes5) {
+		if (op == oper.shl) {
+			return a << r;
+		} else if (op == oper.shr) {
+			return a >> r;
+		} else {
+			revert();
+		}
+	}
+
+	function op_u8_5(oper op, bytes5 a, bytes5 b) pure public returns (bytes5) {
+		if (op == oper.or) {
+			return a | b;
+		} else if (op == oper.and) {
+			return a & b;
+		} else if (op == oper.xor) {
+			return a ^ b;
+		} else {
+			revert();
+		}
+	}
+
+	function op_u8_14_shift(oper op, bytes14 a, uint64 r) pure public returns (bytes14) {
+		if (op == oper.shl) {
+			return a << r;
+		} else if (op == oper.shr) {
+			return a >> r;
+		} else {
+			revert();
+		}
+	}
+
+	function op_u8_14(oper op, bytes14 a, bytes14 b) pure public returns (bytes14) {
+		if (op == oper.or) {
+			return a | b;
+		} else if (op == oper.and) {
+			return a & b;
+		} else if (op == oper.xor) {
+			return a ^ b;
+		} else {
+			revert();
+		}
+	}
+
+	function address_passthrough(address a) pure public returns (address) {
+		return a;
+	}
+}

+ 127 - 0
integration/substrate/primitives.spec.ts

@@ -0,0 +1,127 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+
+describe('Deploy primitives contract and test', () => {
+    it('primitives', async function () {
+        this.timeout(100000);
+
+        let conn = await createConnection();
+        const alice = aliceKeypair();
+
+        let deployed_contract = await deploy(conn, alice, 'primitives.contract');
+
+        let contract = new ContractPromise(conn, deployed_contract.abi, deployed_contract.address);
+
+        // TEST Basic enums
+        // in ethereum, an enum is described as an uint8 so can't use the enum
+        // names programmatically. 0 = add, 1 = sub, 2 = mul, 3 = div, 4 = mod, 5 = pow, 6 = shl, 7 = shr
+        let res = await contract.query.isMul(alice.address, {}, 2);
+        expect(res.output?.toJSON()).toEqual(true);
+
+        res = await contract.query.returnDiv(alice.address, {});
+        expect(res.output?.toJSON()).toEqual("div");
+
+        // TEST uint and int types, and arithmetic/bitwise ops
+        res = await contract.query.opI64(alice.address, {}, 0, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(5100);
+        res = await contract.query.opI64(alice.address, {}, 1, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(-3100);
+        res = await contract.query.opI64(alice.address, {}, 2, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(4100000);
+        res = await contract.query.opI64(alice.address, {}, 3, 1000, 10);
+        expect(res.output?.toJSON()).toEqual(100);
+        res = await contract.query.opI64(alice.address, {}, 4, 1000, 99);
+        expect(res.output?.toJSON()).toEqual(10);
+        res = await contract.query.opI64(alice.address, {}, 6, - 1000, 8);
+        expect(res.output?.toJSON()).toEqual(-256000);
+        res = await contract.query.opI64(alice.address, {}, 7, - 1000, 8);
+        expect(res.output?.toJSON()).toEqual(-4);
+
+
+        res = await contract.query.opU64(alice.address, {}, 0, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(5100);
+        res = await contract.query.opU64(alice.address, {}, 1, 1000, 4100);
+        expect(res.output?.toString()).toEqual("18446744073709548516"); // (2^64)-18446744073709548516 = 3100
+        res = await contract.query.opU64(alice.address, {}, 2, 123456789, 123456789);
+        expect(res.output?.toString()).toEqual("15241578750190521");
+        res = await contract.query.opU64(alice.address, {}, 3, 123456789, 100);
+        expect(res.output?.toJSON()).toEqual(1234567);
+        res = await contract.query.opU64(alice.address, {}, 4, 123456789, 100);
+        expect(res.output?.toJSON()).toEqual(89);
+        res = await contract.query.opU64(alice.address, {}, 5, 3, 7);
+        expect(res.output?.toJSON()).toEqual(2187);
+        res = await contract.query.opI64(alice.address, {}, 6, 1000, 8);
+        expect(res.output?.toJSON()).toEqual(256000);
+        res = await contract.query.opI64(alice.address, {}, 7, 1000, 8);
+        expect(res.output?.toJSON()).toEqual(3);
+
+        // // now for 256 bit operations
+        res = await contract.query.opI256(alice.address, {}, 0, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(5100);
+        res = await contract.query.opI256(alice.address, {}, 1, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(-3100);
+        res = await contract.query.opI256(alice.address, {}, 2, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(4100000);
+        res = await contract.query.opI256(alice.address, {}, 3, 1000, 10);
+        expect(res.output?.toJSON()).toEqual(100);
+        res = await contract.query.opI256(alice.address, {}, 4, 1000, 99);
+        expect(res.output?.toJSON()).toEqual(10);
+        res = await contract.query.opI256(alice.address, {}, 6, - 10000000000000, 8);
+        expect(res.output?.toJSON()).toEqual(-2560000000000000);
+        res = await contract.query.opI256(alice.address, {}, 7, - 10000000000000, 8);
+        expect(res.output?.toJSON()).toEqual(-39062500000);
+
+        res = await contract.query.opU256(alice.address, {}, 0, 1000, 4100);
+        expect(res.output?.toJSON()).toEqual(5100);
+        res = await contract.query.opU256(alice.address, {}, 1, 1000, 4100);
+        expect(res.output?.toString()).toEqual('115792089237316195423570985008687907853269984665640564039457584007913129636836'); // (2^64)-18446744073709548516 = 3100
+        res = await contract.query.opU256(alice.address, {}, 2, 123456789, 123456789);
+        expect(res.output?.toString()).toEqual('15241578750190521');
+        res = await contract.query.opU256(alice.address, {}, 3, 123456789, 100);
+        expect(res.output?.toJSON()).toEqual(1234567);
+        res = await contract.query.opU256(alice.address, {}, 4, 123456789, 100);
+        expect(res.output?.toJSON()).toEqual(89);
+        res = await contract.query.opU256(alice.address, {}, 5, 123456789, 9);
+        expect(res.output?.toString()).toEqual('6662462759719942007440037531362779472290810125440036903063319585255179509');
+        res = await contract.query.opI256(alice.address, {}, 6, 10000000000000, 8);
+        expect(res.output?.toJSON()).toEqual(2560000000000000);
+        res = await contract.query.opI256(alice.address, {}, 7, 10000000000000, 8);
+        expect(res.output?.toJSON()).toEqual(39062500000);
+
+        // TEST bytesN
+        res = await contract.query.returnU86(alice.address, {},);
+        expect(res.output?.toJSON()).toEqual('0x414243444546');
+
+        // TEST bytes5
+        res = await contract.query.opU85Shift(alice.address, {}, 6, '0xdeadcafe59', 8);
+        expect(res.output?.toJSON()).toEqual('0xadcafe5900');
+        res = await contract.query.opU85Shift(alice.address, {}, 7, '0xdeadcafe59', 8);
+        expect(res.output?.toJSON()).toEqual('0x00deadcafe');
+        res = await contract.query.opU85(alice.address, {}, 8, '0xdeadcafe59', '0x0000000006');
+        expect(res.output?.toJSON()).toEqual('0xdeadcafe5f');
+        res = await contract.query.opU85(alice.address, {}, 9, '0xdeadcafe59', '0x00000000ff');
+        expect(res.output?.toJSON()).toEqual('0x0000000059');
+        res = await contract.query.opU85(alice.address, {}, 10, '0xdeadcafe59', '0x00000000ff');
+        expect(res.output?.toJSON()).toEqual('0xdeadcafea6');
+
+        // TEST bytes14
+        res = await contract.query.opU814Shift(alice.address, {}, 6, '0xdeadcafe123456789abcdefbeef7', 9);
+        expect(res.output?.toJSON()).toEqual('0x5b95fc2468acf13579bdf7ddee00');
+        res = await contract.query.opU814Shift(alice.address, {}, 7, '0xdeadcafe123456789abcdefbeef7', 9);
+        expect(res.output?.toJSON()).toEqual('0x006f56e57f091a2b3c4d5e6f7df7');
+        res = await contract.query.opU814(alice.address, {}, 8, '0xdeadcafe123456789abcdefbeef7', '0x0000060000000000000000000000');
+        expect(res.output?.toJSON()).toEqual('0xdeadcefe123456789abcdefbeef7');
+        res = await contract.query.opU814(alice.address, {}, 9, '0xdeadcafe123456789abcdefbeef7', '0x000000000000000000ff00000000');
+        expect(res.output?.toJSON()).toEqual('0x000000000000000000bc00000000');
+        res = await contract.query.opU814(alice.address, {}, 10, '0xdeadcafe123456789abcdefbeef7', '0xff00000000000000000000000000');
+        expect(res.output?.toJSON()).toEqual('0x21adcafe123456789abcdefbeef7');
+
+        // TEST address type.
+        const default_account = '5GBWmgdFAMqm8ZgAHGobqDqX6tjLxJhv53ygjNtaaAn3sjeZ';
+
+        res = await contract.query.addressPassthrough(alice.address, {}, default_account);
+        expect(res.output?.toJSON()).toEqual(default_account);
+        conn.disconnect();
+    });
+});

+ 66 - 0
integration/substrate/store.sol

@@ -0,0 +1,66 @@
+ // ethereum solc wants his pragma
+pragma abicoder v2;
+
+contract store {
+    enum enum_bar { bar1, bar2, bar3, bar4 }
+    uint64 u64;
+    uint32 u32;
+    int16 i16;
+    int256 i256;
+    uint256 u256;
+    string str;
+    bytes bs = hex"b00b1e";
+    bytes4 fixedbytes;
+    enum_bar bar;
+
+    function set_values() public {
+        u64 = type(uint64).max;
+        u32 = 0xdad0feef;
+        i16 = 0x7ffe;
+        i256 = type(int256).max;
+        u256 = 102;
+        str = "the course of true love never did run smooth";
+        fixedbytes = "ABCD";
+        bar = enum_bar.bar2;
+    }
+
+    function get_values1() public view returns (uint64, uint32, int16, int256) {
+        return (u64, u32, i16, i256);
+    }
+
+    function get_values2() public view returns (uint256, string memory, bytes memory, bytes4, enum_bar) {
+        return (u256, str, bs, fixedbytes, bar);
+    }
+
+    function do_ops() public {
+        // u64 will overflow to 1
+        u64 += 2;
+        u32 &= 0xffff;
+        // another overflow
+        i16 += 1;
+        i256 ^= 1;
+        u256 *= 600;
+        str = "";
+        bs[1] = 0xff;
+        // make upper case
+        fixedbytes |= 0x20202020;
+        bar = enum_bar.bar4;
+    }
+
+    function push_zero() public {
+        bs.push();
+    }
+
+    function push(bytes1 b) public {
+        bs.push(b);
+    }
+
+    function pop() public returns (byte) {
+        // note: ethereum solidity bytes.pop() does not return a value
+        return bs.pop();
+    }
+
+    function get_bs() public view returns (bytes memory) {
+        return bs;
+    }
+}

+ 113 - 0
integration/substrate/store.spec.ts

@@ -0,0 +1,113 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+
+describe('Deploy store contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('store', async function () {
+        this.timeout(100000);
+
+        const alice = aliceKeypair();
+
+        let deployed_contract = await deploy(conn, alice, 'store.contract');
+
+        let contract = new ContractPromise(conn, deployed_contract.abi, deployed_contract.address);
+
+        let res1 = await contract.query.getValues1(alice.address, {});
+
+        expect(res1.output?.toJSON()).toStrictEqual([0, 0, 0, 0]);
+
+        let res2 = await contract.query.getValues2(alice.address, {});
+
+        expect(res2.output?.toJSON()).toStrictEqual([0, "", "0xb00b1e", "0x00000000", "bar1"]);
+
+        const tx1 = contract.tx.setValues({ gasLimit });
+
+        await transaction(tx1, alice);
+
+        let res3 = await contract.query.getValues1(alice.address, {});
+
+        expect(res3.output?.toJSON()).toStrictEqual(["0xffffffffffffffff",
+            3671129839,
+            32766,
+            "0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+        ]);
+
+        let res4 = await contract.query.getValues2(alice.address, {});
+
+        expect(res4.output?.toJSON()).toStrictEqual([
+            102,
+            "the course of true love never did run smooth",
+            "0xb00b1e",
+            "0x41424344",
+            "bar2",
+        ]);
+
+        const tx2 = contract.tx.doOps({ gasLimit });
+
+        await transaction(tx2, alice);
+
+        let res5 = await contract.query.getValues1(alice.address, {});
+
+        expect(res5.output?.toJSON()).toStrictEqual([
+            1,
+            65263,
+            32767,
+            "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
+        ]);
+
+        let res6 = await contract.query.getValues2(alice.address, {});
+
+        expect(res6.output?.toJSON()).toStrictEqual([
+            61200,
+            "",
+            "0xb0ff1e",
+            "0x61626364",
+            "bar4",
+        ]);
+
+        const tx3 = contract.tx.pushZero({ gasLimit });
+
+        await transaction(tx3, alice);
+
+        let bs = "0xb0ff1e00";
+
+        for (let i = 0; i < 20; i++) {
+            let res7 = await contract.query.getBs(alice.address, {});
+
+            expect(res7.output?.toJSON()).toStrictEqual(bs);
+
+            if (bs.length <= 4 || Math.random() >= 0.5) {
+                let val = ((Math.random() * 256) | 0).toString(16);
+
+                val = val.length == 1 ? "0" + val : val;
+
+                const tx = contract.tx.push({ gasLimit }, ["0x" + val]);
+
+                await transaction(tx, alice);
+
+                bs += val;
+            } else {
+                const tx = contract.tx.pop({ gasLimit });
+
+                await transaction(tx, alice);
+
+                // note that substrate does not give us access to the return values of a transaction;
+                // therefore, we can't check the return values of pop
+
+                bs = bs.slice(0, -2);
+            }
+
+        }
+    });
+});

+ 100 - 0
integration/substrate/structs.sol

@@ -0,0 +1,100 @@
+ // ethereum solc wants his pragma
+pragma abicoder v2;
+
+contract structs {
+    enum enum_bar { bar1, bar2, bar3, bar4 }
+    struct struct_foo {
+        enum_bar f1;
+        bytes f2;
+        int64 f3;
+        bytes3 f4;
+        string f5;
+        inner_foo f6;
+    }
+
+    struct inner_foo {
+        bool in1;
+        string in2;
+    }
+
+    struct_foo foo1;
+    struct_foo foo2;
+
+    // function for setting the in2 field in either contract storage or memory
+    function set_storage_in2(struct_foo storage f, string memory v) internal {
+        f.f6.in2 = v;
+    }
+
+    // A memory struct is passed by memory reference (pointer)
+    function set_in2(struct_foo memory f, string memory v) pure internal {
+        f.f6.in2 = v;
+    }
+
+    function get_both_foos() public view returns (struct_foo memory, struct_foo memory) {
+        return (foo1, foo2);
+    }
+
+    function get_foo(bool first) public view returns (struct_foo memory) {
+        struct_foo storage f;
+
+        if (first) {
+            f = foo1;
+        } else {
+            f = foo2;
+        }
+
+        return f;
+    }
+
+    function set_foo2(struct_foo f, string v) public {
+        set_in2(f, v);
+        foo2 = f;
+    }
+
+    function pass_foo2(struct_foo f, string v) public pure returns (struct_foo) {
+        set_in2(f, v);
+        return f;
+    }
+
+    function set_foo1() public {
+        foo1.f1 = enum_bar.bar2;
+        foo1.f2 = "Don't count your chickens before they hatch";
+        foo1.f3 = -102;
+        foo1.f4 = hex"edaeda";
+        foo1.f5 = "You can't have your cake and eat it too";
+        foo1.f6.in1 = true;
+
+        set_storage_in2(foo1, 'There are other fish in the sea');
+    }
+
+    function delete_foo(bool first) public {
+        struct_foo storage f;
+
+        if (first) {
+            f = foo1;
+        } else {
+            f = foo2;
+        }
+
+        delete f;
+    }
+
+    function struct_literal() public {
+        // declare a struct literal with fields. There is an
+        // inner struct literal which uses positions
+        struct_foo literal = struct_foo({
+            f1: enum_bar.bar4,
+            f2: "Supercalifragilisticexpialidocious",
+            f3: 0xeffedead1234,
+            f4: unicode'€',
+            f5: "Antidisestablishmentarianism",
+            f6: inner_foo(true, "Pseudopseudohypoparathyroidism")
+        });
+
+        // a literal is just a regular memory struct which can be modified
+        literal.f3 = 0xfd9f;
+
+        // now assign it to a storage variable; it will be copied to contract storage
+        foo1 = literal;
+    }
+}

+ 137 - 0
integration/substrate/structs.spec.ts

@@ -0,0 +1,137 @@
+import expect from 'expect';
+import { gasLimit, createConnection, deploy, transaction, aliceKeypair, } from './index';
+import { ContractPromise } from '@polkadot/api-contract';
+import { ApiPromise } from '@polkadot/api';
+
+describe('Deploy struct contract and test', () => {
+    let conn: ApiPromise;
+
+    before(async function () {
+        conn = await createConnection();
+    });
+
+    after(async function () {
+        await conn.disconnect();
+    });
+
+    it('structs', async function () {
+        this.timeout(50000);
+
+        const alice = aliceKeypair();
+
+        let deployed_contract = await deploy(conn, alice, 'structs.contract');
+
+        let contract = new ContractPromise(conn, deployed_contract.abi, deployed_contract.address);
+
+        const tx1 = contract.tx.setFoo1({ gasLimit });
+
+        await transaction(tx1, alice);
+
+        let res1 = await contract.query.getBothFoos(alice.address, {});
+
+        expect(res1.output?.toJSON()).toStrictEqual([
+            {
+                "f1": "bar2",
+                "f2": "0x446f6e277420636f756e7420796f757220636869636b656e73206265666f72652074686579206861746368",
+                "f3": -102,
+                "f4": "0xedaeda",
+                "f5": "You can't have your cake and eat it too",
+                "f6": { "in1": true, "in2": "There are other fish in the sea" }
+            },
+            {
+                "f1": "bar1",
+                "f2": "0x",
+                "f3": 0,
+                "f4": "0x000000",
+                "f5": "",
+                "f6": { "in1": false, "in2": "" }
+            }
+        ]);
+
+        const tx2 = contract.tx.setFoo2({ gasLimit },
+            {
+                "f1": "bar2",
+                "f2": "0xb52b073595ccb35eaebb87178227b779",
+                "f3": -123112321,
+                "f4": "0x123456",
+                "f5": "Barking up the wrong tree",
+                "f6": {
+                    "in1": true, "in2": "Drive someone up the wall"
+                }
+            },
+            "nah"
+        );
+
+        await transaction(tx2, alice);
+
+        if (1) {
+            let res3 = await contract.query.getFoo(alice.address, {}, false);
+
+            expect(res3.output?.toJSON()).toStrictEqual(
+                {
+                    "f1": "bar2",
+                    "f2": "0xb52b073595ccb35eaebb87178227b779",
+                    "f3": -123112321,
+                    "f4": "0x123456",
+                    "f5": "Barking up the wrong tree",
+                    "f6": { "in1": true, "in2": "nah" }
+                },
+            );
+        }
+
+        let res2 = await contract.query.getBothFoos(alice.address, {});
+
+        expect(res2.output?.toJSON()).toStrictEqual([
+            {
+                "f1": "bar2",
+                "f2": "0x446f6e277420636f756e7420796f757220636869636b656e73206265666f72652074686579206861746368",
+                "f3": -102,
+                "f4": "0xedaeda",
+                "f5": "You can't have your cake and eat it too",
+                "f6": { "in1": true, "in2": "There are other fish in the sea" }
+            },
+            {
+                "f1": "bar2",
+                "f2": "0xb52b073595ccb35eaebb87178227b779",
+                "f3": -123112321,
+                "f4": "0x123456",
+                "f5": "Barking up the wrong tree",
+                "f6": { "in1": true, "in2": "nah" }
+            }
+        ]);
+
+        const tx3 = contract.tx.deleteFoo({ gasLimit }, true);
+
+        await transaction(tx3, alice);
+
+        let res3 = await contract.query.getFoo(alice.address, {}, false);
+
+        expect(res3.output?.toJSON()).toStrictEqual(
+            {
+                "f1": "bar2",
+                "f2": "0xb52b073595ccb35eaebb87178227b779",
+                "f3": -123112321,
+                "f4": "0x123456",
+                "f5": "Barking up the wrong tree",
+                "f6": { "in1": true, "in2": "nah" }
+            },
+        );
+
+        const tx4 = contract.tx.structLiteral({ gasLimit });
+
+        await transaction(tx4, alice);
+
+        let res4 = await contract.query.getFoo(alice.address, {}, true);
+
+        expect(res4.output?.toJSON()).toStrictEqual(
+            {
+                "f1": "bar4",
+                "f2": "0x537570657263616c6966726167696c697374696365787069616c69646f63696f7573",
+                "f3": 64927,
+                "f4": "0xe282ac",
+                "f5": "Antidisestablishmentarianism",
+                "f6": { "in1": true, "in2": "Pseudopseudohypoparathyroidism" },
+            },
+        );
+    });
+});

+ 4 - 0
integration/substrate/to-do

@@ -0,0 +1,4 @@
+ - make create_contract work
+ - transfer/balance
+ - block number
+ - selfdestruct/deposit

+ 62 - 0
integration/substrate/tsconfig.json

@@ -0,0 +1,62 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+    /* Basic Options */
+    // "incremental": true,                   /* Enable incremental compilation */
+    "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+    "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+    // "lib": [],                             /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                       /* Allow javascript files to be compiled. */
+    // "checkJs": true,                       /* Report errors in .js files. */
+    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
+    // "outFile": "./",                       /* Concatenate and emit output to single file. */
+    // "outDir": "./",                        /* Redirect output structure to the directory. */
+    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                     /* Enable project compilation */
+    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
+    // "removeComments": true,                /* Do not emit comments to output. */
+    // "noEmit": true,                        /* Do not emit outputs. */
+    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+    /* Strict Type-Checking Options */
+    "strict": true, /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,              /* Enable strict null checks. */
+    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
+    /* Additional Checks */
+    // "noUnusedLocals": true,                /* Report errors on unused locals. */
+    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
+    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
+    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
+    /* Module Resolution Options */
+    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                       /* List of folders to include type definitions from. */
+    // "types": [],                           /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
+    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */
+    /* Source Map Options */
+    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+    /* Experimental Options */
+    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
+    /* Advanced Options */
+    "skipLibCheck": true, /* Skip type checking of declaration files. */
+    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+  }
+}

+ 14 - 11
src/abi/substrate.rs

@@ -243,19 +243,21 @@ impl Abi {
 
     /// Returns index to builtin type in registry. Type is added if not already present
     fn builtin_enum_type(&mut self, e: &ast::EnumDecl) -> usize {
+        let mut variants: Vec<EnumVariant> = e
+            .values
+            .iter()
+            .map(|(key, val)| EnumVariant {
+                name: key.to_owned(),
+                discriminant: val.1,
+            })
+            .collect();
+
+        variants.sort_by(|a, b| a.discriminant.partial_cmp(&b.discriminant).unwrap());
+
         self.register_ty(Type::Enum {
             path: vec![e.name.to_owned()],
             def: EnumDef {
-                variant: Enum {
-                    variants: e
-                        .values
-                        .iter()
-                        .map(|(key, val)| EnumVariant {
-                            name: key.to_owned(),
-                            discriminant: val.1,
-                        })
-                        .collect(),
-                },
+                variant: Enum { variants },
             },
         })
     }
@@ -368,7 +370,8 @@ fn gen_abi(contract_no: usize, ns: &ast::Namespace) -> Abi {
         .filter_map(|layout| {
             let var = &ns.contracts[layout.contract_no].variables[layout.var_no];
 
-            if !var.ty.contains_mapping(ns) {
+            // mappings and large types cannot be represented
+            if !var.ty.contains_mapping(ns) && var.ty.fits_in_memory(ns) {
                 Some(StorageLayout {
                     name: var.name.to_string(),
                     layout: LayoutField {

+ 1 - 1
src/codegen/cfg.rs

@@ -711,7 +711,7 @@ impl ControlFlowGraph {
                 ty.to_string(ns),
             ),
             Instr::SetStorage { ty, value, storage } => format!(
-                "store storage slot({}) ty:{} = %{}",
+                "store storage slot({}) ty:{} = {}",
                 self.expr_to_string(contract, ns, storage),
                 ty.to_string(ns),
                 self.expr_to_string(contract, ns, value),

+ 2 - 2
src/codegen/storage.rs

@@ -27,7 +27,7 @@ pub fn array_offset(
         Expression::Add(*loc, slot_ty, Box::new(start), Box::new(index))
     } else if (elem_size.clone() & (elem_size.clone() - BigInt::one())) == BigInt::zero() {
         // elem_size is power of 2
-        Expression::ShiftLeft(
+        Expression::Add(
             *loc,
             slot_ty.clone(),
             Box::new(start),
@@ -38,7 +38,7 @@ pub fn array_offset(
                 Box::new(Expression::NumberLiteral(
                     *loc,
                     slot_ty,
-                    BigInt::from_u64(elem_size.bits()).unwrap(),
+                    BigInt::from_u64(elem_size.bits() - 1).unwrap(),
                 )),
             )),
         )

+ 6 - 2
src/emit/mod.rs

@@ -890,7 +890,9 @@ pub trait TargetRuntime<'a> {
                         ns,
                     );
 
-                    if !field.ty.is_reference_type() {
+                    if !field.ty.is_reference_type()
+                        || matches!(field.ty, ast::Type::String | ast::Type::DynamicBytes)
+                    {
                         *slot = bin.builder.build_int_add(
                             *slot,
                             bin.number_literal(256, &field.ty.storage_slots(ns), ns),
@@ -1064,7 +1066,9 @@ pub trait TargetRuntime<'a> {
                 for (_, field) in ns.structs[*n].fields.iter().enumerate() {
                     self.storage_delete_slot(bin, &field.ty, slot, slot_ptr, function, ns);
 
-                    if !field.ty.is_reference_type() {
+                    if !field.ty.is_reference_type()
+                        || matches!(field.ty, ast::Type::String | ast::Type::DynamicBytes)
+                    {
                         *slot = bin.builder.build_int_add(
                             *slot,
                             bin.number_literal(256, &field.ty.storage_slots(ns), ns),

+ 23 - 10
src/emit/substrate.rs

@@ -1142,7 +1142,15 @@ impl SubstrateTarget {
                 };
             }
             ast::Type::Enum(n) => {
-                self.encode_primitive(binary, load, &ns.enums[*n].ty, *data, arg, ns);
+                let arglen = self.encode_primitive(binary, load, &ns.enums[*n].ty, *data, arg, ns);
+
+                *data = unsafe {
+                    binary.builder.build_gep(
+                        *data,
+                        &[binary.context.i32_type().const_int(arglen, false)],
+                        "",
+                    )
+                };
             }
             ast::Type::Array(_, dim) if dim[0].is_some() => {
                 let arg = if load {
@@ -2930,17 +2938,22 @@ impl<'a> TargetRuntime<'a> for SubstrateTarget {
         binary.builder.build_unreachable();
     }
 
-    fn assert_failure<'b>(&self, binary: &'b Binary, data: PointerValue, length: IntValue) {
-        binary.builder.build_call(
-            binary.module.get_function("seal_return").unwrap(),
-            &[
-                binary.context.i32_type().const_int(1, false).into(),
-                data.into(),
-                length.into(),
-            ],
-            "",
+    fn assert_failure<'b>(&self, binary: &'b Binary, _data: PointerValue, _length: IntValue) {
+        // insert "unreachable" instruction; not that build_unreachable() tells the compiler
+        // that this code path is not reachable and may be discarded.
+        let asm_fn = binary.context.void_type().fn_type(&[], false);
+
+        let asm = binary.context.create_inline_asm(
+            asm_fn,
+            "unreachable".to_string(),
+            "".to_string(),
+            true,
+            false,
+            None,
         );
 
+        binary.builder.build_call(asm, &[], "unreachable");
+
         binary.builder.build_unreachable();
     }
 

+ 58 - 17
tests/substrate.rs

@@ -505,7 +505,26 @@ impl Externals for TestRuntime {
 
                 self.vm.input = input;
 
-                let ret = self.invoke_call(module);
+                let ret = module.invoke_export("call", &[], self);
+
+                let ret = match ret {
+                    Err(wasmi::Error::Trap(trap)) => match trap.kind() {
+                        TrapKind::Host(host_error) => {
+                            if let Some(ret) = host_error.downcast_ref::<HostCodeReturn>() {
+                                Some(RuntimeValue::I32(ret.0))
+                            } else if host_error.downcast_ref::<HostCodeTerminate>().is_some() {
+                                Some(RuntimeValue::I32(1))
+                            } else {
+                                return Err(trap);
+                            }
+                        }
+                        _ => {
+                            return Err(trap);
+                        }
+                    },
+                    Ok(v) => v,
+                    Err(e) => panic!("fail to invoke call: {}", e),
+                };
 
                 let output = self.vm.output.clone();
 
@@ -655,7 +674,22 @@ impl Externals for TestRuntime {
 
                 self.vm.input = input;
 
-                let ret = self.invoke_deploy(module);
+                let ret = match module.invoke_export("deploy", &[], self) {
+                    Err(wasmi::Error::Trap(trap)) => match trap.kind() {
+                        TrapKind::Host(host_error) => {
+                            if let Some(ret) = host_error.downcast_ref::<HostCodeReturn>() {
+                                Some(RuntimeValue::I32(ret.0))
+                            } else {
+                                return Err(trap);
+                            }
+                        }
+                        _ => {
+                            return Err(trap);
+                        }
+                    },
+                    Ok(v) => v,
+                    Err(e) => panic!("fail to invoke deploy: {}", e),
+                };
 
                 let output = self.vm.output.clone();
 
@@ -1018,21 +1052,23 @@ impl TestRuntime {
         }
     }
 
-    pub fn function_expect_return(&mut self, name: &str, args: Vec<u8>, expected_ret: i32) {
+    pub fn function_expect_failure(&mut self, name: &str, args: Vec<u8>) {
         let m = self.abi.get_function(name).unwrap();
 
         let module = self.create_module(&self.accounts.get(&self.vm.address).unwrap().0);
 
         self.vm.input = m.selector().into_iter().chain(args).collect();
 
-        if let Some(RuntimeValue::I32(ret)) = self.invoke_call(module) {
-            println!(
-                "function_expected_return: got {} expected {}",
-                ret, expected_ret
-            );
-
-            if expected_ret != ret {
-                panic!("non one return")
+        match module.invoke_export("call", &[], self) {
+            Err(wasmi::Error::Trap(trap)) => match trap.kind() {
+                TrapKind::Unreachable => (),
+                _ => panic!("trap: {:?}", trap),
+            },
+            Err(err) => {
+                panic!("unexpected error: {:?}", err);
+            }
+            Ok(v) => {
+                panic!("unexpected return value: {:?}", v);
             }
         }
     }
@@ -1049,16 +1085,21 @@ impl TestRuntime {
         }
     }
 
-    pub fn raw_function_return(&mut self, expect_ret: i32, input: Vec<u8>) {
+    pub fn raw_function_failure(&mut self, input: Vec<u8>) {
         let module = self.create_module(&self.accounts.get(&self.vm.address).unwrap().0);
 
         self.vm.input = input;
 
-        if let Some(RuntimeValue::I32(ret)) = self.invoke_call(module) {
-            println!("got {} expected {}", ret, expect_ret);
-
-            if ret != expect_ret {
-                panic!("return not expected")
+        match module.invoke_export("call", &[], self) {
+            Err(wasmi::Error::Trap(trap)) => match trap.kind() {
+                TrapKind::Unreachable => (),
+                _ => panic!("trap: {:?}", trap),
+            },
+            Err(err) => {
+                panic!("unexpected error: {:?}", err);
+            }
+            Ok(v) => {
+                panic!("unexpected return value: {:?}", v);
             }
         }
     }

+ 2 - 2
tests/substrate_tests/arrays.rs

@@ -1896,9 +1896,9 @@ fn large_index_ty_in_bounds() {
     runtime.constructor(0, Vec::new());
     runtime.function("test", 15u128.encode());
 
-    runtime.function_expect_return("test", 17u128.encode(), 1);
+    runtime.function_expect_failure("test", 17u128.encode());
 
-    runtime.function_expect_return("test", 0xfffffffffffffu128.encode(), 1);
+    runtime.function_expect_failure("test", 0xfffffffffffffu128.encode());
 }
 
 #[test]

+ 2 - 2
tests/substrate_tests/builtins.rs

@@ -924,7 +924,7 @@ fn addmod() {
         }"##,
     );
 
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 
     // bigger numbers (64 bit)
     let mut runtime = build_solidity(
@@ -1012,7 +1012,7 @@ fn mulmod() {
         }"##,
     );
 
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 
     // bigger numbers
     let mut runtime = build_solidity(

+ 20 - 34
tests/substrate_tests/calls.rs

@@ -34,23 +34,13 @@ fn revert() {
         }"##,
     );
 
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 
-    assert_eq!(
-        runtime.vm.output,
-        RevertReturn(0x08c3_79a0, "yo!".to_string()).encode()
-    );
+    assert_eq!(runtime.vm.output.len(), 0);
 
-    runtime.function_expect_return("a", Vec::new(), 1);
+    runtime.function_expect_failure("a", Vec::new());
 
-    assert_eq!(
-        runtime.vm.output,
-        RevertReturn(
-            0x08c3_79a0,
-            "revert value has to be passed down the stack".to_string()
-        )
-        .encode()
-    );
+    assert_eq!(runtime.vm.output.len(), 0);
 
     let mut runtime = build_solidity(
         r##"
@@ -61,7 +51,7 @@ fn revert() {
         }"##,
     );
 
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 
     assert_eq!(runtime.vm.output.len(), 0);
 }
@@ -81,12 +71,10 @@ fn require() {
         }"##,
     );
 
-    runtime.function_expect_return("test1", Vec::new(), 1);
+    runtime.function_expect_failure("test1", Vec::new());
 
-    assert_eq!(
-        runtime.vm.output,
-        RevertReturn(0x08c3_79a0, "Program testing can be used to show the presence of bugs, but never to show their absence!".to_string()).encode()
-    );
+    // The reason is lost
+    assert_eq!(runtime.vm.output.len(), 0);
 
     runtime.function("test2", Vec::new());
 
@@ -103,10 +91,10 @@ fn input_wrong_size() {
         }"##,
     );
 
-    runtime.function_expect_return("test", b"A".to_vec(), 1);
+    runtime.function_expect_failure("test", b"A".to_vec());
 
     // the decoder does check if there is too much data
-    runtime.function_expect_return("test", b"ABCDE".to_vec(), 1);
+    runtime.function_expect_failure("test", b"ABCDE".to_vec());
 }
 
 #[test]
@@ -128,7 +116,7 @@ fn external_call_not_exist() {
         }"##,
     );
 
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 }
 
 #[test]
@@ -150,7 +138,7 @@ fn contract_already_exists() {
         }"##,
     );
 
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 
     let mut runtime = build_solidity(
         r##"
@@ -281,7 +269,7 @@ fn try_catch_external_calls() {
         "##,
     );
 
-    runtime.function("test", Vec::new());
+    runtime.function_expect_failure("test", Vec::new());
 
     let ns = parse_and_resolve(
         r##"
@@ -454,7 +442,7 @@ fn try_catch_external_calls() {
         "##,
     );
 
-    runtime.function("test", Vec::new());
+    runtime.function_expect_failure("test", Vec::new());
 
     #[derive(Debug, PartialEq, Encode, Decode)]
     struct Ret(u32);
@@ -512,9 +500,7 @@ fn try_catch_external_calls() {
 
     runtime.function("create_child", Vec::new());
 
-    runtime.function("test", Vec::new());
-
-    assert_eq!(runtime.vm.output, Ret(4000).encode());
+    runtime.function_expect_failure("test", Vec::new());
 }
 
 #[test]
@@ -648,7 +634,7 @@ fn try_catch_constructor() {
         "##,
     );
 
-    runtime.function("test", Vec::new());
+    runtime.function_expect_failure("test", Vec::new());
 
     let ns = parse_and_resolve(
         r##"
@@ -854,7 +840,7 @@ fn payable_functions() {
 
     runtime.constructor(0, Vec::new());
     runtime.vm.value = 1;
-    runtime.function_expect_return("test", Vec::new(), 1);
+    runtime.function_expect_failure("test", Vec::new());
 
     // test both
     let mut runtime = build_solidity(
@@ -869,7 +855,7 @@ fn payable_functions() {
 
     runtime.constructor(0, Vec::new());
     runtime.vm.value = 1;
-    runtime.function_expect_return("test2", Vec::new(), 1);
+    runtime.function_expect_failure("test2", Vec::new());
     runtime.vm.value = 1;
     runtime.function("test", Vec::new());
 
@@ -942,7 +928,7 @@ fn payable_functions() {
     assert_eq!(runtime.vm.output, Ret(3).encode());
 
     runtime.vm.value = 0;
-    runtime.raw_function_return(1, b"abde".to_vec());
+    runtime.raw_function_failure(b"abde".to_vec());
     let mut runtime = build_solidity(
         r##"
         contract c {
@@ -964,7 +950,7 @@ fn payable_functions() {
 
     runtime.constructor(0, Vec::new());
     runtime.vm.value = 1;
-    runtime.raw_function_return(1, b"abde".to_vec());
+    runtime.raw_function_failure(b"abde".to_vec());
 
     runtime.vm.value = 0;
     runtime.raw_function(b"abde".to_vec());

+ 3 - 16
tests/substrate_tests/contracts.rs

@@ -422,12 +422,7 @@ fn revert_external_call() {
 
     runtime.constructor(0, Vec::new());
 
-    runtime.function_expect_return("test", Vec::new(), 1);
-
-    assert_eq!(
-        runtime.vm.output,
-        RevertReturn(0x08c3_79a0, "The reason why".to_string()).encode()
-    );
+    runtime.function_expect_failure("test", Vec::new());
 }
 
 #[test]
@@ -459,17 +454,9 @@ fn revert_constructor() {
 
     runtime.constructor(0, Vec::new());
 
-    runtime.function_expect_return("test", Vec::new(), 1);
-
-    let expected = RevertReturn(0x08c3_79a0, "Hello, World!".to_string()).encode();
-
-    println!(
-        "{} == {}",
-        hex::encode(&runtime.vm.output),
-        hex::encode(&expected)
-    );
+    runtime.function_expect_failure("test", Vec::new());
 
-    assert_eq!(runtime.vm.output, expected);
+    assert_eq!(runtime.vm.output.len(), 0);
 }
 
 #[test]

+ 1 - 1
tests/substrate_tests/value.rs

@@ -807,7 +807,7 @@ fn selfdestruct() {
     runtime.function("step1", Vec::new());
     assert_eq!(runtime.accounts.get_mut(&runtime.vm.address).unwrap().1, 0);
 
-    runtime.function_expect_return("step2", Vec::new(), 1);
+    runtime.function_expect_failure("step2", Vec::new());
     assert_eq!(
         runtime.accounts.get_mut(&runtime.vm.address).unwrap().1,
         511