diff --git a/ownership-chain/e2e-tests/tests/config.ts b/ownership-chain/e2e-tests/tests/config.ts index b5cfe007a..6bfe0e8c9 100644 --- a/ownership-chain/e2e-tests/tests/config.ts +++ b/ownership-chain/e2e-tests/tests/config.ts @@ -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 \ No newline at end of file diff --git a/ownership-chain/e2e-tests/tests/test-create-collection.ts b/ownership-chain/e2e-tests/tests/test-create-collection.ts index dbc9c2923..de6e0b804 100644 --- a/ownership-chain/e2e-tests/tests/test-create-collection.ts +++ b/ownership-chain/e2e-tests/tests/test-create-collection.ts @@ -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" @@ -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++ }); 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); diff --git a/ownership-chain/e2e-tests/tests/test-evolution.ts b/ownership-chain/e2e-tests/tests/test-evolution.ts new file mode 100644 index 000000000..9ede7e4f2 --- /dev/null +++ b/ownership-chain/e2e-tests/tests/test-evolution.ts @@ -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] + ) + ); + }); +}); \ No newline at end of file diff --git a/ownership-chain/e2e-tests/tests/util.ts b/ownership-chain/e2e-tests/tests/util.ts index 36074fec7..515c57872 100644 --- a/ownership-chain/e2e-tests/tests/util.ts +++ b/ownership-chain/e2e-tests/tests/util.ts @@ -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; @@ -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 +} \ No newline at end of file diff --git a/ownership-chain/precompile/laos-evolution/src/lib.rs b/ownership-chain/precompile/laos-evolution/src/lib.rs index c2d605535..972d8f8c0 100644 --- a/ownership-chain/precompile/laos-evolution/src/lib.rs +++ b/ownership-chain/precompile/laos-evolution/src/lib.rs @@ -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. @@ -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)] @@ -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 => { diff --git a/ownership-chain/precompile/laos-evolution/src/tests.rs b/ownership-chain/precompile/laos-evolution/src/tests.rs index c8f4bc693..44c50d96e 100644 --- a/ownership-chain/precompile/laos-evolution/src/tests.rs +++ b/ownership-chain/precompile/laos-evolution/src/tests.rs @@ -36,7 +36,7 @@ fn check_log_selectors() { ); assert_eq!( hex::encode(SELECTOR_LOG_EVOLVED_WITH_EXTERNAL_TOKEN_URI), - "568b059e9377ea804907ac57dc8d56446b17dbf9f4b30dfe1935b9c8815ae7e1" + "95c167d04a267f10e6b3f373c7a336dc65cf459caf048854dc32a2d37ab1607c" ); }