Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addition of End-to-End Tests for Evolution Flow #138

Merged
merged 15 commits into from
Nov 6, 2023
23 changes: 20 additions & 3 deletions ownership-chain/e2e-tests/tests/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
export const NODE_BINARY_NAME = "laos-ownership";
import LaosEvolution from "../build/contracts/LaosEvolution.json";
import { AbiItem } from "web3-utils";
import BN from "bn.js";


// Node config
export const RUNTIME_SPEC_NAME = "frontier-template";
export const RUNTIME_SPEC_VERSION = 7;
export const RUNTIME_IMPL_VERSION = 0;
export const RPC_PORT = 9999;

// Chain config
export const CHAIN_ID = 667;

export const GENESIS_ACCOUNT = "0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d";
export const GENESIS_ACCOUNT_PRIVATE_KEY = "0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df";
export const GENESIS_ACCOUNT_BALANCE = "77559934324363988854052928524572160";
export const GENESIS_ACCOUNT_BALANCE = "77559934324363988853790420524572160";
export const GAS_PRICE = "0x3B9ACA00";
export const GAS = "0x10000";

// LAOS Evolution Contract
export const LAOS_EVOLUTION_ABI = LaosEvolution.abi as AbiItem[]
export const CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000403";
export const SELECTOR_LOG_NEW_COLLECTION = "0x6eb24fd767a7bcfa417f3fe25a2cb245d2ae52293d3c4a8f8c6450a09795d289";
export const SELECTOR_LOG_MINTED_WITH_EXTERNAL_TOKEN_URI = "0x4b3b5da28a351f8bb73b960d7c80b2cef3e3570cb03448234dee173942c74786";
export const SELECTOR_LOG_EVOLVED_WITH_EXTERNAL_TOKEN_URI = "0x95c167d04a267f10e6b3f373c7a336dc65cf459caf048854dc32a2d37ab1607c";


export const MAX_U96 = new BN('79228162514264337593543950336'); // 2^96 - 1
42 changes: 22 additions & 20 deletions ownership-chain/e2e-tests/tests/test-create-collection.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
import { customRequest, describeWithExistingNode } from "./util";
import { describeWithExistingNode } from "./util";
import { step } from "mocha-steps";
import { GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY } from "./config";
import LaosEvolution from "../build/contracts/LaosEvolution.json";
import { AbiItem } from "web3-utils";
import { CONTRACT_ADDRESS, GAS, GAS_PRICE, GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY, LAOS_EVOLUTION_ABI, SELECTOR_LOG_NEW_COLLECTION } from "./config";
import { expect } from "chai";
import Contract from "web3-eth-contract";


describeWithExistingNode("Frontier RPC (Create Collection)", (context) => {
const LAOS_EVOLUTION_ABI = LaosEvolution.abi as AbiItem[]
const CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000403";
const contract = new context.web3.eth.Contract(LAOS_EVOLUTION_ABI, CONTRACT_ADDRESS, {
from: GENESIS_ACCOUNT,
gasPrice: "0x3B9ACA00",
let contract: Contract;
let nonce: number;

beforeEach(async function () {
contract = new context.web3.eth.Contract(LAOS_EVOLUTION_ABI, CONTRACT_ADDRESS, {
from: GENESIS_ACCOUNT,
gasPrice: GAS_PRICE,
});

nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT);
context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY);
});

step("when collection does not exist owner of call should fail", async function () {
const collectionId = "0";
try {
await contract.methods.ownerOfCollection(collectionId).call();
expect.fail("Expected error was not thrown"); // Ensure an error is thrown
} catch (error) {
expect(error.message).to.be.eq(
"Returned error: VM Exception while processing transaction: revert"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a generic error message, is there any way to expect more specific message?

Copy link
Contributor Author

@magecnion magecnion Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be a solution? https://ethereum.stackexchange.com/a/84551

Expand All @@ -28,25 +35,20 @@ describeWithExistingNode("Frontier RPC (Create Collection)", (context) => {
this.timeout(70000);

const collectionId = "0";
let nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT);

await context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY);
const result = await contract.methods.createCollection(GENESIS_ACCOUNT).send({ from: GENESIS_ACCOUNT, gas: "0x10000", nonce: nonce++ });

const result = await contract.methods.createCollection(GENESIS_ACCOUNT).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
magecnion marked this conversation as resolved.
Show resolved Hide resolved
expect(result.status).to.be.eq(true);

const owner = await contract.methods.ownerOfCollection(collectionId).call();
expect(owner).to.be.eq(GENESIS_ACCOUNT);
});

step("when collection is created event is emitted", async function () {
this.timeout(70000);

const collectionId = "1";
let nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT);
const SELECTOR_LOG_NEW_COLLECTION = "0x6eb24fd767a7bcfa417f3fe25a2cb245d2ae52293d3c4a8f8c6450a09795d289";

await context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY);
const result = await contract.methods.createCollection(GENESIS_ACCOUNT).send({ from: GENESIS_ACCOUNT, gas: "0x10000", nonce: nonce++ });
const result = await contract.methods.createCollection(GENESIS_ACCOUNT).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(result.status).to.be.eq(true);

expect(Object.keys(result.events).length).to.be.eq(1);
Expand Down
162 changes: 162 additions & 0 deletions ownership-chain/e2e-tests/tests/test-evolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { describeWithExistingNode, slotAndOwnerToTokenId } from "./util";
import { step } from "mocha-steps";
import { CONTRACT_ADDRESS, GAS, GAS_PRICE, GENESIS_ACCOUNT, GENESIS_ACCOUNT_PRIVATE_KEY, LAOS_EVOLUTION_ABI, SELECTOR_LOG_EVOLVED_WITH_EXTERNAL_TOKEN_URI, SELECTOR_LOG_MINTED_WITH_EXTERNAL_TOKEN_URI, SELECTOR_LOG_NEW_COLLECTION } from "./config";
import { expect } from "chai";
import Contract from "web3-eth-contract";
import BN from "bn.js";

describeWithExistingNode("Frontier RPC (Mint and Evolve Assets)", (context) => {
let contract: Contract;
let nonce: number;
let collectionId: number = 0;

beforeEach(async function () {
this.timeout(70000);

contract = new context.web3.eth.Contract(LAOS_EVOLUTION_ABI, CONTRACT_ADDRESS, {
from: GENESIS_ACCOUNT,
gasPrice: GAS_PRICE,
});

nonce = await context.web3.eth.getTransactionCount(GENESIS_ACCOUNT);

context.web3.eth.accounts.wallet.add(GENESIS_ACCOUNT_PRIVATE_KEY);

const result = await contract.methods.createCollection(GENESIS_ACCOUNT).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(result.status).to.be.eq(true);
collectionId = result.events.NewCollection.returnValues.collectionId;
});

step("when collection does not exist token uri should fail", async function () {
const tokenId = "0";

try {
await contract.methods.tokenURI(collectionId, tokenId).call();
expect.fail("Expected error was not thrown"); // Ensure an error is thrown
} catch (error) {
expect(error.message).to.be.eq(
"Returned error: VM Exception while processing transaction: revert"
);
}
});

step("when asset is minted it should return token uri", async function () {
this.timeout(70000);

const slot = "0";
const to = GENESIS_ACCOUNT;
const tokenURI = "https://example.com";

const result = await contract.methods.mintWithExternalURI(collectionId, slot, to, tokenURI).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(result.status).to.be.eq(true);

const tokenId = result.events.MintedWithExternalURI.returnValues.tokenId;
const got = await contract.methods.tokenURI(collectionId, tokenId).call();
expect(got).to.be.eq(tokenURI);
});

step("given slot and owner it should return token id", async function () {
this.timeout(70000);

const slot = "1";
const to = GENESIS_ACCOUNT;

const tokenId = slotAndOwnerToTokenId(slot, to);
expect(tokenId).to.be.eq("000000000000000000000001c0f0f4ab324c46e55d02d0033343b4be8a55532d");
const tokenIdDecimal = new BN(tokenId, 16, "be").toString(10);
expect(tokenIdDecimal).to.be.eq("2563001357829637001682277476112176020532353127213");
});

step("when asset is minted it should emit an event", async function () {
this.timeout(70000);

const slot = "22";
const to = GENESIS_ACCOUNT;
const tokenURI = "https://example.com";

const result = await contract.methods.mintWithExternalURI(collectionId, slot, to, tokenURI)
.send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(result.status).to.be.eq(true);

expect(Object.keys(result.events).length).to.be.eq(1);

// data returned within the event
expect(result.events.MintedWithExternalURI.returnValues.collectionId).to.be.eq(collectionId);
expect(result.events.MintedWithExternalURI.returnValues.slot).to.be.eq(slot);
expect(result.events.MintedWithExternalURI.returnValues.to).to.be.eq(to);
expect(result.events.MintedWithExternalURI.returnValues.tokenURI).to.be.eq(tokenURI);
const tokenId = slotAndOwnerToTokenId(slot, to);
const tokenIdDecimal = new BN(tokenId, 16, "be").toString(10);
expect(result.events.MintedWithExternalURI.returnValues.tokenId).to.be.eq(tokenIdDecimal);

// event topics
expect(result.events.MintedWithExternalURI.raw.topics.length).to.be.eq(2);
expect(result.events.MintedWithExternalURI.raw.topics[0]).to.be.eq(SELECTOR_LOG_MINTED_WITH_EXTERNAL_TOKEN_URI);
expect(result.events.MintedWithExternalURI.raw.topics[1]).to.be.eq(context.web3.utils.padLeft(GENESIS_ACCOUNT.toLowerCase(), 64));

// event data
expect(result.events.MintedWithExternalURI.raw.data).to.be.eq(
context.web3.eth.abi.encodeParameters(
["uint64", "uint96", "string", "uint256"],
[collectionId, slot, tokenURI, tokenIdDecimal]
)
);
});

step("when asset is evolved it should change token uri", async function () {
this.timeout(70000);

const slot = "22";
const to = GENESIS_ACCOUNT;
const tokenURI = "https://example.com";
const newTokenURI = "https://new_example.com";
const tokenId = slotAndOwnerToTokenId(slot, to);
const tokenIdDecimal = new BN(tokenId, 16, "be").toString(10);

const mintingResult = await contract.methods.mintWithExternalURI(collectionId, slot, to, tokenURI).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(mintingResult.status).to.be.eq(true);

const evolvingResult = await contract.methods.evolveWithExternalURI(collectionId, tokenIdDecimal, newTokenURI).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(evolvingResult.status).to.be.eq(true);

const got = await contract.methods.tokenURI(collectionId, tokenIdDecimal).call();
expect(got).to.be.eq(newTokenURI);
});

step("when asset is evolved it should emit an event", async function () {
this.timeout(70000);

const slot = "22";
const to = GENESIS_ACCOUNT;
const tokenURI = "https://example.com";
const newTokenURI = "https://new_example.com";
const tokenId = slotAndOwnerToTokenId(slot, to);
const tokenIdDecimal = new BN(tokenId, 16, "be").toString(10);

const mintingResult = await contract.methods.mintWithExternalURI(collectionId, slot, to, tokenURI).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(mintingResult.status).to.be.eq(true);

const evolvingResult = await contract.methods.evolveWithExternalURI(collectionId, tokenIdDecimal, newTokenURI).send({ from: GENESIS_ACCOUNT, gas: GAS, nonce: nonce++ });
expect(evolvingResult.status).to.be.eq(true);

expect(Object.keys(evolvingResult.events).length).to.be.eq(1);

// data returned within the event
expect(evolvingResult.events.EvolvedWithExternalURI.returnValues.collectionId).to.be.eq(collectionId);
expect(evolvingResult.events.EvolvedWithExternalURI.returnValues.tokenId).to.be.eq(tokenIdDecimal);
expect(evolvingResult.events.EvolvedWithExternalURI.returnValues.tokenURI).to.be.eq(newTokenURI);

// event topics
expect(evolvingResult.events.EvolvedWithExternalURI.raw.topics.length).to.be.eq(2);
expect(evolvingResult.events.EvolvedWithExternalURI.raw.topics[0]).to.be.eq(SELECTOR_LOG_EVOLVED_WITH_EXTERNAL_TOKEN_URI);
expect(evolvingResult.events.EvolvedWithExternalURI.raw.topics[1]).to.be.eq("0x" + tokenId);

// event data
expect(evolvingResult.events.EvolvedWithExternalURI.raw.data).to.be.eq(
context.web3.eth.abi.encodeParameters(
["uint64", "string"],
[collectionId, newTokenURI]
)
);
});
});
31 changes: 27 additions & 4 deletions ownership-chain/e2e-tests/tests/util.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { ethers } from "ethers";
import Web3 from "web3";
import { JsonRpcResponse } from "web3-core-helpers";

import { CHAIN_ID } from "./config";

export const RPC_PORT = 9999;
import { MAX_U96, RPC_PORT } from "./config";
import BN from "bn.js";

require("events").EventEmitter.prototype._maxListeners = 100;

Expand Down Expand Up @@ -50,3 +48,28 @@ export function describeWithExistingNode(title: string, cb: (context: { web3: We
});
}

/**
* Converts a slot and owner address to a token ID.
* @param slot The slot number.
* @param owner The owner address.
* @returns The token ID, or null if the slot is larger than 96 bits or the owner address is not 20 bytes.
*/
export function slotAndOwnerToTokenId(slot: string, owner: string): string | null {

const slotBN: BN = new BN(slot);
const ownerBytes: Uint8Array = Uint8Array.from(Buffer.from(owner.slice(2), 'hex')); // Remove the '0x' prefix and convert hex to bytes

if (slotBN.gt(MAX_U96) || ownerBytes.length != 20){
return null;
}

// Convert slot to big-endian byte array
const slotBytes = slotBN.toArray('be', 16); // 16 bytes (128 bits)

// We also use the last 12 bytes of the slot, since the first 4 bytes are always 0
let bytes = new Uint8Array(32);
bytes.set(slotBytes.slice(-12), 0); // slice from the right to ensure we get the least significant bytes
bytes.set(ownerBytes, 12);

return Buffer.from(bytes).toString('hex'); // Convert Uint8Array to hexadecimal string
}
5 changes: 3 additions & 2 deletions ownership-chain/precompile/laos-evolution/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use precompile_utils::{
};

use sp_core::H160;
use sp_runtime::DispatchError;
use sp_std::{fmt::Debug, marker::PhantomData, vec::Vec};

/// Solidity selector of the CreateCollection log, which is the Keccak of the Log signature.
Expand All @@ -18,7 +19,7 @@ pub const SELECTOR_LOG_NEW_COLLECTION: [u8; 32] = keccak256!("NewCollection(uint
pub const SELECTOR_LOG_MINTED_WITH_EXTERNAL_TOKEN_URI: [u8; 32] =
keccak256!("MintedWithExternalURI(uint64,uint96,address,string,uint256)");
pub const SELECTOR_LOG_EVOLVED_WITH_EXTERNAL_TOKEN_URI: [u8; 32] =
keccak256!("EvolvedWithExternalURI(uint256,uint64,string)");
keccak256!("EvolvedWithExternalURI(uint64,uint256,string)");

#[precompile_utils_macro::generate_function_selector]
#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -89,7 +90,7 @@ where
if let Some(owner) = LaosEvolution::collection_owner(collection_id) {
Ok(succeed(EvmDataWriter::new().write(Address(owner.into())).build()))
} else {
Err(revert("collection does not exist"))
Err(revert_dispatch_error(DispatchError::Other("collection does not exist")))
}
},
Action::TokenURI => {
Expand Down
2 changes: 1 addition & 1 deletion ownership-chain/precompile/laos-evolution/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn check_log_selectors() {
);
assert_eq!(
hex::encode(SELECTOR_LOG_EVOLVED_WITH_EXTERNAL_TOKEN_URI),
"568b059e9377ea804907ac57dc8d56446b17dbf9f4b30dfe1935b9c8815ae7e1"
"95c167d04a267f10e6b3f373c7a336dc65cf459caf048854dc32a2d37ab1607c"
);
}

Expand Down