diff --git a/frontend/src/contracts.ts b/frontend/src/contracts.ts index f3dd90a7a..ecf400bc8 100644 --- a/frontend/src/contracts.ts +++ b/frontend/src/contracts.ts @@ -72,6 +72,7 @@ import { StakingContractEntry, stakingContractsInfo, nftStakingContractsInfo } f import {raid, pvp, quests, burningManager} from './feature-flags'; import {currentChainSupportsPvP, currentChainSupportsQuests} from '@/utils/common'; +import {abi as multicallAbi} from './data/Multicall.json'; interface RaidContracts { Raid1?: Contracts['Raid1']; @@ -368,6 +369,10 @@ export async function setUpContracts(web3: Web3): Promise { const treasuryContractAddr = getConfigValue('VUE_APP_TREASURY_CONTRACT_ADDRESS') || (treasuryNetworks as Networks)[networkId]!.address; const Treasury = new web3.eth.Contract(treasuryAbi as Abi, treasuryContractAddr); + const multicallAddr = getConfigValue('VUE_APP_MULTICALL_CONTRACT_ADDRESS'); + console.log('multicallAddr', multicallAddr); + const MultiCall = new web3.eth.Contract(multicallAbi as Abi, multicallAddr); + let BurningManager; if(burningManager) { const burningManagerContractAddr = getConfigValue('VUE_APP_BURNING_MANAGER_CONTRACT_ADDRESS') || (burningManagerNetworks as Networks)[networkId]!.address; @@ -413,6 +418,7 @@ export async function setUpContracts(web3: Web3): Promise { KingStakingRewardsUpgradeable, KingStakingRewardsUpgradeable90, KingStakingRewardsUpgradeable180, - SpecialWeaponsManager + SpecialWeaponsManager, + MultiCall }; } diff --git a/frontend/src/data/Multicall.json b/frontend/src/data/Multicall.json new file mode 100644 index 000000000..10f11d911 --- /dev/null +++ b/frontend/src/data/Multicall.json @@ -0,0 +1,35 @@ +{ + "contractName": "MultiCall", + "abi": [ + { + "constant": true, + "inputs": [ + { + "components": [ + { "name": "target", "type": "address" }, + { "name": "callData", "type": "bytes" } + ], + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { "name": "blockNumber", "type": "uint256" }, + { "name": "returnData", "type": "bytes[]" } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [{ "name": "addr", "type": "address" }], + "name": "getEthBalance", + "outputs": [{ "name": "balance", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ] +} diff --git a/frontend/src/interfaces/Contracts.ts b/frontend/src/interfaces/Contracts.ts index f6058460b..42846ca35 100644 --- a/frontend/src/interfaces/Contracts.ts +++ b/frontend/src/interfaces/Contracts.ts @@ -8,6 +8,7 @@ import type { WeaponCosmetics, CharacterCosmetics, NFTStorage, CBKLandSale, CBKLand, Treasury, Promos, BurningManager, SimpleQuests, PartnerVault, SpecialWeaponsManager, PvpCore, PvpRankings, TokensManager } from '../../../build/abi-interfaces'; +import { MultiCall } from './Multicall'; import { StakeType, NftStakeType } from './State'; interface TypeSafeContract { @@ -70,4 +71,5 @@ export interface Contracts { SimpleQuests?: Contract; PartnerVault?: Contract; SpecialWeaponsManager?: Contract; + MultiCall: Contract; } diff --git a/frontend/src/interfaces/Multicall.ts b/frontend/src/interfaces/Multicall.ts new file mode 100644 index 000000000..43e2cea7a --- /dev/null +++ b/frontend/src/interfaces/Multicall.ts @@ -0,0 +1,21 @@ +import { Web3JsAbiCall } from '../../../abi-common'; + +export interface getNFTCall { + abi: any; + calls: callData[]; +} + +export interface callData { + address: string; + name: string; + params: string[]; +} + +export interface returnData { + blockNumber: number; + returnData: string[]; +} + +export interface MultiCall { + aggregate(calldata: string[]): Web3JsAbiCall; +} diff --git a/frontend/src/interfaces/index.ts b/frontend/src/interfaces/index.ts index edeed76db..0609c491a 100644 --- a/frontend/src/interfaces/index.ts +++ b/frontend/src/interfaces/index.ts @@ -4,3 +4,4 @@ export * from './State'; export * from './Target'; export * from './Weapon'; export * from './Contracts'; +export * from './Multicall'; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 00d1e68ad..d8d4165f5 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -2,7 +2,13 @@ import Vue from 'vue'; import Vuex from 'vuex'; import Web3 from 'web3'; import _ from 'lodash'; -import {bnMinimum, currentChainSupportsDrawbridge, currentChainSupportsPvP, currentChainSupportsQuests, toBN} from '@/utils/common'; +import { + bnMinimum, + currentChainSupportsDrawbridge, + currentChainSupportsPvP, + currentChainSupportsQuests, + toBN, +} from '@/utils/common'; import {getConfigValue, setUpContracts} from '@/contracts'; @@ -16,11 +22,17 @@ import {burningManager as featureFlagBurningManager} from '@/feature-flags'; import {ERC20, IERC721, INftStakingRewards, IStakingRewards} from '@/../../build/abi-interfaces'; import {stakeTypeThatCanHaveUnclaimedRewardsStakedTo} from '@/stake-types'; import {Nft} from '@/interfaces/Nft'; +import { Interface } from '@ethersproject/abi'; import {Element} from '@/enums/Element'; import {getWeaponNameFromSeed} from '@/weapon-name'; import axios from 'axios'; import {abi as erc20Abi} from '@/../../build/contracts/ERC20.json'; import {abi as erc721Abi} from '@/../../build/contracts/IERC721.json'; +import { abi as charactersAbi } from '@/../../build/contracts/Characters.json'; +import { abi as weaponsAbi } from '@/../../build/contracts/Weapons.json'; +import { abi as shieldsAbi } from '@/../../build/contracts/Shields.json'; +import { abi as raidTrinketsAbi } from '@/../../build/contracts/RaidTrinket.json'; +import { abi as junkAbi } from '@/../../build/contracts/Junk.json'; import BigNumber from 'bignumber.js'; import bridge from './bridge'; import pvp from './pvp'; @@ -31,6 +43,7 @@ import land from './land'; import treasury from './treasury'; import specialWeaponsManager from './specialWeaponsManager'; import combat from './combat'; +import { getNFTCall } from '@/utils/multicall'; const transakAPIURL = process.env.VUE_APP_TRANSAK_API_URL || 'https://staging-global.transak.com'; const transakAPIKey = process.env.VUE_APP_TRANSAK_API_KEY || '90167697-74a7-45f3-89da-c24d32b9606c'; @@ -1085,23 +1098,57 @@ export default new Vuex.Store({ await dispatch('fetchSkillBalance'); }, - async fetchCharacters({ dispatch }, characterIds: (string | number)[]) { - await Promise.all(characterIds.map(id => dispatch('fetchCharacter', { characterId: id }))); + async fetchCharacters({ state, dispatch }, characterIds: (string | number)[]) { + const { Characters } = state.contracts(); + if (!Characters) return; + + console.log('fetch 1'); + console.log('address: ', Characters?.options.address); + console.log('getNFTCall: ', getNFTCall(charactersAbi, Characters?.options.address, 'get', characterIds.map(characterId => [characterId]))); + const multiCharacterDatas: string[] = await dispatch( + 'multicall', + getNFTCall(charactersAbi, Characters?.options.address, 'get', characterIds.map(characterId => [characterId]))); + console.log('fetch 2'); + characterIds.forEach((characterId, i) => { + dispatch('fetchCharacter', { characterId, characterData: multiCharacterDatas[i] }); + }); + console.log('fetch 3'); }, - async fetchGarrisonCharacters({ dispatch }, garrisonCharacterIds: (string | number)[]) { - await Promise.all(garrisonCharacterIds.map(id => dispatch('fetchCharacter', { characterId: id, inGarrison: true }))); + async fetchGarrisonCharacters({ state, dispatch }, garrisonCharacterIds: (string | number)[]) { + const { Characters } = state.contracts(); + if (!Characters) return; + + const multiCharacterDatas: string[] = await dispatch( + 'multicall', + getNFTCall(charactersAbi, Characters?.options.address, 'get', garrisonCharacterIds.map(garrisonCharacterId => [garrisonCharacterId]))); + + garrisonCharacterIds.forEach((garrisonCharacterId, i) => { + dispatch('fetchCharacter', { characterId: garrisonCharacterId, characterData: multiCharacterDatas[i], inGarrison: true }); + }); }, - async fetchCharacter({ state, commit, dispatch }, { characterId, inGarrison = false }: { characterId: string | number, inGarrison: boolean}) { + /** + * + * @param param0 object containing references to relevant globals + * @param param1 object containing the main params. They are: + * - characterId: characterId of the character being fetched + * - characterData: the optional character data gotten from use of multiCall + * - inGarrison: true if from fetchGarrisonCharacters + */ + async fetchCharacter( + { state, commit, dispatch }, + { characterId, characterData = [], inGarrison = false }: + { characterId: string | number, characterData: string[], inGarrison: boolean}) { + const { Characters } = state.contracts(); - if(!Characters) return; + if (!Characters) return; await Promise.all([ (async () => { const character = characterFromContract( characterId, - await Characters.methods.get('' + characterId).call(defaultCallOptions(state)) + characterData.length > 0 ? characterData : await Characters.methods.get('' + characterId).call(defaultCallOptions(state)) ); await dispatch('fetchCharacterPower', characterId); await dispatch('getIsCharacterInArena', characterId); @@ -1130,11 +1177,25 @@ export default new Vuex.Store({ } }, - async fetchWeapons({ dispatch }, weaponIds: (string | number)[]) { - await Promise.all(weaponIds.map(id => dispatch('fetchWeapon', id))); + async fetchWeapons({ state, dispatch }, weaponIds: (string | number)[]) { + const { Weapons } = state.contracts(); + if(!Weapons) return; + + const multiWeaponDatas: string[] = await dispatch( + 'multicall', + getNFTCall(weaponsAbi, Weapons?.options.address, 'get', weaponIds.map(weaponId => [weaponId]))); + + weaponIds.forEach((weaponId, i) => { + dispatch('fetchCharacter', { weaponId, weaponData: multiWeaponDatas[i] }); + }); }, - async fetchWeapon({ state, commit, dispatch }, weaponId: string | number) { + /** + * + * @param weaponId weaponId of the weapon being fetched + * @param weaponData the optional weapon data gotten from use of multiCall + */ + async fetchWeapon({ state, commit, dispatch }, weaponId: string | number, weaponData: string[] = []) { const { Weapons } = state.contracts(); if(!Weapons) return; @@ -1142,7 +1203,7 @@ export default new Vuex.Store({ (async () => { const weapon = weaponFromContract( weaponId, - await Weapons.methods.get('' + weaponId).call(defaultCallOptions(state)) + weaponData.length > 0 ? weaponData : await Weapons.methods.get('' + weaponId).call(defaultCallOptions(state)) ); commit('updateWeapon', { weaponId, weapon }); @@ -1155,11 +1216,26 @@ export default new Vuex.Store({ if(!Shields || !state.defaultAccount) return; return await Shields.methods.getNftVar(shieldId, 2).call(defaultCallOptions(state)); }, - async fetchShields({ dispatch }, shieldIds: (string | number)[]) { - await Promise.all(shieldIds.map(id => dispatch('fetchShield', id))); + + async fetchShields({ state, dispatch }, shieldIds: (string | number)[]) { + const { Shields } = state.contracts(); + if(!Shields) return; + + const multiShieldDatas: string[] = await dispatch( + 'multicall', + getNFTCall(shieldsAbi, Shields?.options.address, 'get', shieldIds.map(shieldId => [shieldId]))); + + shieldIds.forEach((shieldId, i) => { + dispatch('fetchShield', { shieldId, shieldData: multiShieldDatas[i] }); + }); }, - async fetchShield({ state, commit }, shieldId: string | number) { + /** + * + * @param shieldId shieldId of the shield being fetched + * @param shieldData the optional shield data gotten from use of multiCall + */ + async fetchShield({ state, commit }, shieldId: string | number, shieldData: string[] = []) { const { Shields } = state.contracts(); if(!Shields) return; @@ -1167,7 +1243,7 @@ export default new Vuex.Store({ (async () => { const shield = shieldFromContract( shieldId, - await Shields.methods.get('' + shieldId).call(defaultCallOptions(state)) + shieldData.length > 0 ? shieldData : await Shields.methods.get('' + shieldId).call(defaultCallOptions(state)) ); commit('updateShield', { shieldId, shield }); @@ -1175,11 +1251,25 @@ export default new Vuex.Store({ ]); }, - async fetchTrinkets({ dispatch }, trinketIds: (string | number)[]) { - await Promise.all(trinketIds.map(id => dispatch('fetchTrinket', id))); + async fetchTrinkets({ state, dispatch }, trinketIds: (string | number)[]) { + const { RaidTrinket } = state.contracts(); + if(!RaidTrinket) return; + + const multiTrinketDatas: string[] = await dispatch( + 'multicall', + getNFTCall(raidTrinketsAbi, RaidTrinket?.options.address, 'get', trinketIds.map(trinketId => [trinketId]))); + + trinketIds.forEach((trinketId, i) => { + dispatch('fetchTrinket', { trinketId, trinketData: multiTrinketDatas[i] }); + }); }, - async fetchTrinket({ state, commit }, trinketId: string | number) { + /** + * + * @param trinketId trinketId of the trinket being fetched + * @param trinketData the optional trinket data gotten from use of multiCall + */ + async fetchTrinket({ state, commit }, trinketId: string | number, trinketData: string[] = []) { const { RaidTrinket } = state.contracts(); if(!RaidTrinket) return; @@ -1187,7 +1277,7 @@ export default new Vuex.Store({ (async () => { const trinket = trinketFromContract( trinketId, - await RaidTrinket.methods.get('' + trinketId).call(defaultCallOptions(state)) + trinketData.length > 0 ? trinketData : await RaidTrinket.methods.get('' + trinketId).call(defaultCallOptions(state)) ); commit('updateTrinket', { trinketId, trinket }); @@ -1195,11 +1285,25 @@ export default new Vuex.Store({ ]); }, - async fetchJunks({ dispatch }, junkIds: (string | number)[]) { - await Promise.all(junkIds.map(id => dispatch('fetchJunk', id))); + async fetchJunks({ state, dispatch }, junkIds: (string | number)[]) { + const { Junk } = state.contracts(); + if(!Junk) return; + + const multiJunkDatas = await dispatch( + 'multicall', + getNFTCall(junkAbi, Junk?.options.address, 'get', junkIds.map(junkId => [junkId]))); + + junkIds.forEach((junkId, i) => { + dispatch('fetchJunk', { junkId, junkData: multiJunkDatas[i] }); + }); }, - async fetchJunk({ state, commit }, junkId: string | number) { + /** + * + * @param junkId junkId of the junk being fetched + * @param junkData the optional junk data gotten from use of multiCall + */ + async fetchJunk({ state, commit }, junkId: string | number, junkData: string = '') { const { Junk } = state.contracts(); if(!Junk) return; @@ -1207,7 +1311,7 @@ export default new Vuex.Store({ (async () => { const junk = junkFromContract( junkId, - await Junk.methods.get('' + junkId).call(defaultCallOptions(state)) + junkData ? junkData : await Junk.methods.get('' + junkId).call(defaultCallOptions(state)) ); commit('updateJunk', { junkId, junk }); @@ -2769,5 +2873,18 @@ export default new Vuex.Store({ return CryptoBlades.methods.getMintCharacterFee().call(defaultCallOptions(state)); }, + + async multicall({state}, {abi, calls}) { + console.log('in multiCall'); + const { MultiCall } = state.contracts(); + const itf = new Interface(abi); + const data = calls.map((call: any) => [ + call.address.toLowerCase(), + itf.encodeFunctionData(call.name, call.params), + ]); + const { returnData } = await MultiCall.methods.aggregate(data).call(defaultCallOptions(state)) || []; + const res = returnData.map((call, i) => itf.decodeFunctionResult(calls[i].name, call)); + return res; + }, } }); diff --git a/frontend/src/utils/multicall.ts b/frontend/src/utils/multicall.ts new file mode 100644 index 000000000..92aff2c0e --- /dev/null +++ b/frontend/src/utils/multicall.ts @@ -0,0 +1,13 @@ +import { callData, getNFTCall } from '@/interfaces'; + +export function getNFTCall(abi: any, address: any, name: string, params: any[]): getNFTCall { + const calls: callData[] = params.map((param: string[]) => ({ + address, + name, + params: param, + })); + return { + abi, + calls, + }; +}