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

Implement multicall #1631

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion frontend/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -368,6 +369,10 @@ export async function setUpContracts(web3: Web3): Promise<Contracts> {
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');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we want to keep line 109
if(process.env.NODE_ENV === 'development') return '';
This was originally commented out, and it isn't in CB-Marketplace, which is the only reason this would work in a dev environment.

Copy link
Member

Choose a reason for hiding this comment

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

We need to leave it, as it's in place for taking the address from generated abi files (as the addresses change every time you setup a dev env). I don't think we have multicall in our migrations, so it won't load it correctly. We somehow need to address this issue,

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;
Expand Down Expand Up @@ -413,6 +418,7 @@ export async function setUpContracts(web3: Web3): Promise<Contracts> {
KingStakingRewardsUpgradeable,
KingStakingRewardsUpgradeable90,
KingStakingRewardsUpgradeable180,
SpecialWeaponsManager
SpecialWeaponsManager,
MultiCall
};
}
35 changes: 35 additions & 0 deletions frontend/src/data/Multicall.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
2 changes: 2 additions & 0 deletions frontend/src/interfaces/Contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Abi> {
Expand Down Expand Up @@ -70,4 +71,5 @@ export interface Contracts {
SimpleQuests?: Contract<SimpleQuests>;
PartnerVault?: Contract<PartnerVault>;
SpecialWeaponsManager?: Contract<SpecialWeaponsManager>;
MultiCall: Contract<MultiCall>;
}
21 changes: 21 additions & 0 deletions frontend/src/interfaces/Multicall.ts
Original file line number Diff line number Diff line change
@@ -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<returnData>;
}
1 change: 1 addition & 0 deletions frontend/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './State';
export * from './Target';
export * from './Weapon';
export * from './Contracts';
export * from './Multicall';
165 changes: 141 additions & 24 deletions frontend/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -1085,23 +1098,57 @@ export default new Vuex.Store<IState>({
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');
Copy link
Member

Choose a reason for hiding this comment

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

A reminder comment to delete console.logs after the code is in a desired shape.

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])));
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need optional chaining operator "?", as at the start we have if (!Characters) return;

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);
Expand Down Expand Up @@ -1130,19 +1177,33 @@ export default new Vuex.Store<IState>({
}
},

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])));
Copy link
Member

Choose a reason for hiding this comment

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

Same here, "?" not needed


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;

await Promise.all([
(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 });
Expand All @@ -1155,59 +1216,102 @@ export default new Vuex.Store<IState>({
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])));
Copy link
Member

Choose a reason for hiding this comment

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

Same here, "?" not needed


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;

await Promise.all([
(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 });
})(),
]);
},

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])));
Copy link
Member

Choose a reason for hiding this comment

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

Same here, "?" not needed


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;

await Promise.all([
(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 });
})(),
]);
},

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])));
Copy link
Member

Choose a reason for hiding this comment

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

Same here, "?" not needed


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;

await Promise.all([
(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 });
Expand Down Expand Up @@ -2769,5 +2873,18 @@ export default new Vuex.Store<IState>({

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;
},
}
});
13 changes: 13 additions & 0 deletions frontend/src/utils/multicall.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}