--- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -267,7 +267,7 @@ pub fn development_config() -> DefaultChainSpec { let mut properties = Map::new(); properties.insert("tokenSymbol".into(), default_runtime::TOKEN_SYMBOL.into()); - properties.insert("tokenDecimals".into(), 18.into()); + properties.insert("tokenDecimals".into(), default_runtime::DECIMALS.into()); properties.insert( "ss58Format".into(), default_runtime::SS58Prefix::get().into(), @@ -341,7 +341,7 @@ pub fn local_testnet_config() -> DefaultChainSpec { let mut properties = Map::new(); properties.insert("tokenSymbol".into(), default_runtime::TOKEN_SYMBOL.into()); - properties.insert("tokenDecimals".into(), 18.into()); + properties.insert("tokenDecimals".into(), default_runtime::DECIMALS.into()); properties.insert( "ss58Format".into(), default_runtime::SS58Prefix::get().into(), --- a/pallets/balances-adapter/src/erc.rs +++ b/pallets/balances-adapter/src/erc.rs @@ -38,14 +38,14 @@ #[solidity_interface(name = ERC20, events(ERC20Events), enum(derive(PreDispatch)), enum_attr(weight), expect_selector = 0x942e8b22)] impl NativeFungibleHandle { - fn allowance(&self, owner: Address, spender: Address) -> Result { + fn allowance(&self, _owner: Address, _spender: Address) -> Result { Ok(U256::zero()) } // #[weight(>::approve())] - fn approve(&mut self, caller: Caller, spender: Address, amount: U256) -> Result { + fn approve(&mut self, _caller: Caller, _spender: Address, _amount: U256) -> Result { // self.consume_store_reads(1)?; - Err("Approve not supported now".into()) + Err("Approve not supported".into()) } fn balance_of(&self, owner: Address) -> Result { @@ -106,7 +106,7 @@ let to = T::CrossAccountId::from_eth(to); let amount = amount.try_into().map_err(|_| "amount overflow")?; - if from != to { + if from != caller { return Err("no permission".into()); } // let budget = self @@ -171,7 +171,7 @@ let to = to.into_sub_cross_account::()?; let amount = amount.try_into().map_err(|_| "amount overflow")?; - if from != to { + if from != caller { return Err("no permission".into()); } --- a/pallets/common/src/dispatch.rs +++ b/pallets/common/src/dispatch.rs @@ -83,7 +83,7 @@ /// /// * `sender` - The owner of the collection. /// * `handle` - Collection handle. - fn destroy(sender: T::CrossAccountId, handle: CollectionHandle) -> DispatchResult; + fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> DispatchResult; /// Get a specialized collection from the handle. /// --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -1296,10 +1296,7 @@ sender: T::CrossAccountId, collection_id: CollectionId, ) -> DispatchResult { - let collection = >::try_get(collection_id)?; - collection.check_is_internal()?; - - T::CollectionDispatch::destroy(sender, collection)?; + T::CollectionDispatch::destroy(sender, collection_id)?; // TODO: basket cleanup should be moved elsewhere // Maybe runtime dispatch.rs should perform it? --- a/runtime/common/config/pallets/mod.rs +++ b/runtime/common/config/pallets/mod.rs @@ -24,7 +24,8 @@ weights::CommonWeights, RelayChainBlockNumberProvider, }, - Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, RUNTIME_NAME, TOKEN_SYMBOL, Balances, + Runtime, RuntimeEvent, RuntimeCall, RuntimeOrigin, RUNTIME_NAME, TOKEN_SYMBOL, DECIMALS, + Balances, }; use frame_support::traits::{ConstU32, ConstU64, Currency}; use up_common::{ @@ -53,7 +54,7 @@ parameter_types! { pub const CollectionCreationPrice: Balance = 2 * UNIQUE; - pub const Decimals: u8 = 32; + pub const Decimals: u8 = DECIMALS; pub TreasuryAccountId: AccountId = TreasuryModuleId::get().into_account_truncating(); pub Name: String = RUNTIME_NAME.to_string(); pub Symbol: String = TOKEN_SYMBOL.to_string(); --- a/runtime/common/dispatch.rs +++ b/runtime/common/dispatch.rs @@ -99,7 +99,10 @@ Ok(id) } - fn destroy(sender: T::CrossAccountId, collection: CollectionHandle) -> DispatchResult { + fn destroy(sender: T::CrossAccountId, collection_id: CollectionId) -> DispatchResult { + let collection = >::try_get(collection_id)?; + collection.check_is_internal()?; + match collection.mode { CollectionMode::ReFungible => { PalletRefungible::destroy_collection(RefungibleHandle::cast(collection), &sender)? --- a/runtime/opal/src/lib.rs +++ b/runtime/opal/src/lib.rs @@ -45,6 +45,7 @@ pub const RUNTIME_NAME: &str = "opal"; pub const TOKEN_SYMBOL: &str = "OPL"; +pub const DECIMALS: u8 = 18; /// This runtime version. pub const VERSION: RuntimeVersion = RuntimeVersion { --- a/runtime/quartz/src/lib.rs +++ b/runtime/quartz/src/lib.rs @@ -48,6 +48,7 @@ #[cfg(not(feature = "become-sapphire"))] pub const RUNTIME_NAME: &str = "quartz"; pub const TOKEN_SYMBOL: &str = "QTZ"; +pub const DECIMALS: u8 = 18; /// This runtime version. pub const VERSION: RuntimeVersion = RuntimeVersion { --- a/runtime/unique/src/lib.rs +++ b/runtime/unique/src/lib.rs @@ -45,6 +45,7 @@ pub const RUNTIME_NAME: &str = "unique"; pub const TOKEN_SYMBOL: &str = "UNQ"; +pub const DECIMALS: u8 = 18; /// This runtime version. pub const VERSION: RuntimeVersion = RuntimeVersion { --- a/tests/src/eth/nativeFungible.test.ts +++ b/tests/src/eth/nativeFungible.test.ts @@ -15,29 +15,157 @@ // along with Unique Network. If not, see . import {IKeyringPair} from '@polkadot/types/types'; -import {itEth, usingEthPlaygrounds} from './util'; +import {expect, itEth, usingEthPlaygrounds} from './util'; -describe('NativeFungible: Plain calls', () => { +describe('NativeFungible: ERC20 calls', () => { let donor: IKeyringPair; - let alice: IKeyringPair; - let owner: IKeyringPair; before(async function() { await usingEthPlaygrounds(async (helper, privateKey) => { donor = await privateKey({url: import.meta.url}); - [alice, owner] = await helper.arrange.createAccounts([30n, 20n], donor); + // [alice] = await helper.arrange.createAccounts([30n], donor); }); }); - itEth.skip('Can perform approve()', async ({helper}) => { + itEth('approve()', async ({helper}) => { const owner = await helper.eth.createAccountWithBalance(donor); const spender = helper.eth.createAccount(); - const collection = await helper.ft.mintCollection(alice); - await collection.mint(alice, 200n, {Ethereum: owner}); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + await expect(contract.methods.approve(spender, 100).call({from: owner})).to.be.rejectedWith('Approve not supported'); + }); + + itEth('balanceOf()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor, 123n); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const balance = await contract.methods.balanceOf(owner).call({from: owner}); + expect(balance).to.be.eq('123000000000000000000'); + }); + + itEth('decimals()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const decimals = await contract.methods.decimals().call({from: owner}); + expect(decimals).to.be.eq('18'); + }); + + itEth('name()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const name = await contract.methods.name().call({from: owner}); + expect(name).to.be.eq('opal'); + }); + + itEth('symbol()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const name = await contract.methods.symbol().call({from: owner}); + expect(name).to.be.eq('OPL'); + }); + + itEth('totalSupply()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const totalSupplyEth = BigInt(await contract.methods.totalSupply().call({from: owner})); + const totalSupplySub = await helper.balance.getTotalIssuance(); + expect(totalSupplyEth).to.be.eq(totalSupplySub); + }); + + itEth('transfer()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const receiver = await helper.eth.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const balanceOwnerBefore = await helper.balance.getEthereum(owner); + const balanceReceiverBefore = await helper.balance.getEthereum(receiver); + + await contract.methods.transfer(receiver, 50).send({from: owner}); + + const balanceOwnerAfter = await helper.balance.getEthereum(owner); + const balanceReceiverAfter = await helper.balance.getEthereum(receiver); + + expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true; + expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true; + }); + itEth('transferFrom()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const receiver = await helper.eth.createAccountWithBalance(donor); const collectionAddress = helper.ethAddress.fromCollectionId(0); const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); - await contract.methods.approve(spender, 100).send({from: owner}); + const balanceOwnerBefore = await helper.balance.getEthereum(owner); + const balanceReceiverBefore = await helper.balance.getEthereum(receiver); + + await contract.methods.transferFrom(owner, receiver, 50).send({from: owner}); + + const balanceOwnerAfter = await helper.balance.getEthereum(owner); + const balanceReceiverAfter = await helper.balance.getEthereum(receiver); + + expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true; + expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true; + + await expect(contract.methods.transferFrom(receiver, receiver, 50).call({from: owner})).to.be.rejectedWith('no permission'); + }); +}); + +describe('NativeFungible: ERC20UniqueExtensions calls', () => { + let donor: IKeyringPair; + + before(async function() { + await usingEthPlaygrounds(async (helper, privateKey) => { + donor = await privateKey({url: import.meta.url}); + // [alice] = await helper.arrange.createAccounts([30n], donor); + }); + }); + + itEth('transferCross()', async ({helper}) => { + const owner = await helper.eth.createAccountWithBalance(donor); + const receiver = await helper.ethCrossAccount.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner); + + const balanceOwnerBefore = await helper.balance.getEthereum(owner); + const balanceReceiverBefore = await helper.balance.getEthereum(receiver.eth); + + await contract.methods.transferCross(receiver, 50).send({from: owner}); + + const balanceOwnerAfter = await helper.balance.getEthereum(owner); + const balanceReceiverAfter = await helper.balance.getEthereum(receiver.eth); + + expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true; + expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true; + }); + + itEth('transferFromCross()', async ({helper}) => { + const owner = await helper.ethCrossAccount.createAccountWithBalance(donor); + const receiver = await helper.ethCrossAccount.createAccountWithBalance(donor); + const collectionAddress = helper.ethAddress.fromCollectionId(0); + const contract = await helper.ethNativeContract.collection(collectionAddress, 'ft', owner.eth); + + const balanceOwnerBefore = await helper.balance.getEthereum(owner.eth); + const balanceReceiverBefore = await helper.balance.getEthereum(receiver.eth); + + await contract.methods.transferFromCross(owner, receiver, 50).send({from: owner.eth}); + + const balanceOwnerAfter = await helper.balance.getEthereum(owner.eth); + const balanceReceiverAfter = await helper.balance.getEthereum(receiver.eth); + + expect(balanceOwnerBefore - 50n > balanceOwnerAfter).to.be.true; + expect(balanceReceiverBefore === balanceReceiverAfter - 50n).to.be.true; + + await expect(contract.methods.transferFromCross(receiver, receiver, 50).call({from: owner.eth})).to.be.rejectedWith('no permission'); }); }); \ No newline at end of file --- a/tests/src/eth/util/playgrounds/unique.dev.ts +++ b/tests/src/eth/util/playgrounds/unique.dev.ts @@ -142,7 +142,7 @@ async collection(address: string, mode: TCollectionMode, caller?: string, mergeDeprecated = false) { let abi; - if (address === '0x17C4e6453cC49aaAAEaCA894E6d9683e00000000' && mode === 'ft') { + if (address === '0x17C4e6453cC49aaAAEaCA894E6d9683e00000000') { abi = nativeFungibleAbi; } else { abi ={ --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -2400,7 +2400,16 @@ return {free: accountInfo.free.toBigInt(), frozen: accountInfo.frozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()}; } - async getLocked(address: TSubstrateAccount): Promise<[{ id: string, amount: bigint, reason: string }]> { + /** + * Get total issuance + * @returns + */ + async getTotalIssuance(): Promise { + const total = (await this.helper.callRpc('api.query.balances.totalIssuance', [])); + return total.toBigInt(); + } + + async getLocked(address: TSubstrateAccount): Promise<[{id: string, amount: bigint, reason: string}]> { const locks = (await this.helper.callRpc('api.query.balances.locks', [address])).toHuman(); return locks.map((lock: any) => { return {id: lock.id, amount: BigInt(lock.amount.replace(/,/g, '')), reasons: lock.reasons}; }); } @@ -2488,6 +2497,14 @@ } /** + * Get total issuance + * @returns + */ + getTotalIssuance(): Promise { + return this.subBalanceGroup.getTotalIssuance(); + } + + /** * Get locked balances * @param address substrate address * @returns locked balances with reason via api.query.balances.locks