DCIPs/assets/eip-6220/test/equippableSlotParts.ts

1131 lines
33 KiB
TypeScript
Raw Normal View History

import { loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { BigNumber } from "ethers";
import { ethers } from "hardhat";
import {
CatalogMock,
EquippableTokenMock,
EquipRenderUtils,
} from "../typechain-types";
const partIdForBody = 1;
const partIdForWeapon = 2;
const partIdForWeaponGem = 3;
const partIdForBackground = 4;
const uniqueSnakeSoldiers = 10;
const uniqueWeapons = 4;
// const uniqueWeaponGems = 2;
// const uniqueBackgrounds = 3;
const snakeSoldiersIds: number[] = [];
const weaponsIds: number[] = [];
const weaponGemsIds: number[] = [];
const backgroundsIds: number[] = [];
const soldierResId = 100;
const weaponAssetsFull = [1, 2, 3, 4]; // Must match the total of uniqueAssets
const weaponAssetsEquip = [5, 6, 7, 8]; // Must match the total of uniqueAssets
const weaponGemAssetFull = 101;
const weaponGemAssetEquip = 102;
const backgroundAssetId = 200;
enum ItemType {
None,
Slot,
Fixed,
}
let addrs: SignerWithAddress[];
let nextTokenId = 1;
let nextChildTokenId = 100;
async function mint(token: EquippableTokenMock, to: string): Promise<number> {
const tokenId = nextTokenId;
nextTokenId++;
await token["mint(address,uint256)"](to, tokenId);
return tokenId;
}
async function nestMint(
token: EquippableTokenMock,
to: string,
parentId: number
): Promise<number> {
const childTokenId = nextChildTokenId;
nextChildTokenId++;
await token["nestMint(address,uint256,uint256)"](to, childTokenId, parentId);
return childTokenId;
}
async function setupContextForSlots(
catalog: CatalogMock,
soldier: EquippableTokenMock,
weapon: EquippableTokenMock,
weaponGem: EquippableTokenMock,
background: EquippableTokenMock
) {
[, ...addrs] = await ethers.getSigners();
await setupCatalog();
await mintSnakeSoldiers();
await mintWeapons();
await mintWeaponGems();
await mintBackgrounds();
await addAssetsToSoldier();
await addAssetsToWeapon();
await addAssetsToWeaponGem();
await addAssetsToBackground();
return {
catalog,
soldier,
weapon,
background,
};
async function setupCatalog(): Promise<void> {
const partForBody = {
itemType: ItemType.Fixed,
z: 1,
equippable: [],
metadataURI: "genericBody.png",
};
const partForWeapon = {
itemType: ItemType.Slot,
z: 2,
equippable: [weapon.address],
metadataURI: "",
};
const partForWeaponGem = {
itemType: ItemType.Slot,
z: 3,
equippable: [weaponGem.address],
metadataURI: "noGem.png",
};
const partForBackground = {
itemType: ItemType.Slot,
z: 0,
equippable: [background.address],
metadataURI: "noBackground.png",
};
await catalog.addPartList([
{ partId: partIdForBody, part: partForBody },
{ partId: partIdForWeapon, part: partForWeapon },
{ partId: partIdForWeaponGem, part: partForWeaponGem },
{ partId: partIdForBackground, part: partForBackground },
]);
}
async function mintSnakeSoldiers(): Promise<void> {
// This array is reused, so we "empty" it before
snakeSoldiersIds.length = 0;
// Using only first 3 addresses to mint
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
const newId = await mint(soldier, addrs[i % 3].address);
snakeSoldiersIds.push(newId);
}
}
async function mintWeapons(): Promise<void> {
// This array is reused, so we "empty" it before
weaponsIds.length = 0;
// Mint one weapon to soldier
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
const newId = await nestMint(
weapon,
soldier.address,
snakeSoldiersIds[i]
);
weaponsIds.push(newId);
await soldier
.connect(addrs[i % 3])
.acceptChild(snakeSoldiersIds[i], 0, weapon.address, newId);
}
}
async function mintWeaponGems(): Promise<void> {
// This array is reused, so we "empty" it before
weaponGemsIds.length = 0;
// Mint one weapon gem for each weapon on each soldier
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
const newId = await nestMint(weaponGem, weapon.address, weaponsIds[i]);
weaponGemsIds.push(newId);
await weapon
.connect(addrs[i % 3])
.acceptChild(weaponsIds[i], 0, weaponGem.address, newId);
}
}
async function mintBackgrounds(): Promise<void> {
// This array is reused, so we "empty" it before
backgroundsIds.length = 0;
// Mint one background to soldier
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
const newId = await nestMint(
background,
soldier.address,
snakeSoldiersIds[i]
);
backgroundsIds.push(newId);
await soldier
.connect(addrs[i % 3])
.acceptChild(snakeSoldiersIds[i], 0, background.address, newId);
}
}
async function addAssetsToSoldier(): Promise<void> {
await soldier.addEquippableAssetEntry(
soldierResId,
0,
catalog.address,
"ipfs:soldier/",
[partIdForBody, partIdForWeapon, partIdForBackground]
);
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
await soldier.addAssetToToken(snakeSoldiersIds[i], soldierResId, 0);
await soldier
.connect(addrs[i % 3])
.acceptAsset(snakeSoldiersIds[i], 0, soldierResId);
}
}
async function addAssetsToWeapon(): Promise<void> {
const equippableGroupId = 1; // Assets to equip will both use this
for (let i = 0; i < weaponAssetsFull.length; i++) {
await weapon.addEquippableAssetEntry(
weaponAssetsFull[i],
0, // Not meant to equip
ethers.constants.AddressZero, // Not meant to equip
`ipfs:weapon/full/${weaponAssetsFull[i]}`,
[]
);
}
for (let i = 0; i < weaponAssetsEquip.length; i++) {
await weapon.addEquippableAssetEntry(
weaponAssetsEquip[i],
equippableGroupId,
catalog.address,
`ipfs:weapon/equip/${weaponAssetsEquip[i]}`,
[partIdForWeaponGem]
);
}
// Can be equipped into snakeSoldiers
await weapon.setValidParentForEquippableGroup(
equippableGroupId,
soldier.address,
partIdForWeapon
);
// Add 2 assets to each weapon, one full, one for equip
// There are 10 weapon tokens for 4 unique assets so we use %
for (let i = 0; i < weaponsIds.length; i++) {
await weapon.addAssetToToken(
weaponsIds[i],
weaponAssetsFull[i % uniqueWeapons],
0
);
await weapon.addAssetToToken(
weaponsIds[i],
weaponAssetsEquip[i % uniqueWeapons],
0
);
await weapon
.connect(addrs[i % 3])
.acceptAsset(weaponsIds[i], 0, weaponAssetsFull[i % uniqueWeapons]);
await weapon
.connect(addrs[i % 3])
.acceptAsset(weaponsIds[i], 0, weaponAssetsEquip[i % uniqueWeapons]);
}
}
async function addAssetsToWeaponGem(): Promise<void> {
const equippableGroupId = 1; // Assets to equip will use this
await weaponGem.addEquippableAssetEntry(
weaponGemAssetFull,
0, // Not meant to equip
ethers.constants.AddressZero, // Not meant to equip
"ipfs:weagponGem/full/",
[]
);
await weaponGem.addEquippableAssetEntry(
weaponGemAssetEquip,
equippableGroupId,
catalog.address,
"ipfs:weagponGem/equip/",
[]
);
await weaponGem.setValidParentForEquippableGroup(
// Can be equipped into weapons
equippableGroupId,
weapon.address,
partIdForWeaponGem
);
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
await weaponGem.addAssetToToken(weaponGemsIds[i], weaponGemAssetFull, 0);
await weaponGem.addAssetToToken(weaponGemsIds[i], weaponGemAssetEquip, 0);
await weaponGem
.connect(addrs[i % 3])
.acceptAsset(weaponGemsIds[i], 0, weaponGemAssetFull);
await weaponGem
.connect(addrs[i % 3])
.acceptAsset(weaponGemsIds[i], 0, weaponGemAssetEquip);
}
}
async function addAssetsToBackground(): Promise<void> {
const equippableGroupId = 1; // Assets to equip will use this
await background.addEquippableAssetEntry(
backgroundAssetId,
equippableGroupId,
catalog.address,
"ipfs:background/",
[]
);
// Can be equipped into snakeSoldiers
await background.setValidParentForEquippableGroup(
equippableGroupId,
soldier.address,
partIdForBackground
);
for (let i = 0; i < uniqueSnakeSoldiers; i++) {
await background.addAssetToToken(backgroundsIds[i], backgroundAssetId, 0);
await background
.connect(addrs[i % 3])
.acceptAsset(backgroundsIds[i], 0, backgroundAssetId);
}
}
}
async function slotsFixture() {
const catalogSymbol = "SSB";
const catalogType = "mixed";
const catalogFactory = await ethers.getContractFactory("CatalogMock");
const equipFactory = await ethers.getContractFactory("EquippableTokenMock");
const viewFactory = await ethers.getContractFactory("EquipRenderUtils");
// View
const view = <EquipRenderUtils>await viewFactory.deploy();
await view.deployed();
// Catalog
const catalog = <CatalogMock>(
await catalogFactory.deploy(catalogSymbol, catalogType)
);
await catalog.deployed();
// Soldier token
const soldier = <EquippableTokenMock>await equipFactory.deploy();
await soldier.deployed();
// Weapon
const weapon = <EquippableTokenMock>await equipFactory.deploy();
await weapon.deployed();
// Weapon Gem
const weaponGem = <EquippableTokenMock>await equipFactory.deploy();
await weaponGem.deployed();
// Background
const background = <EquippableTokenMock>await equipFactory.deploy();
await background.deployed();
await setupContextForSlots(catalog, soldier, weapon, weaponGem, background);
return { catalog, soldier, weapon, weaponGem, background, view };
}
// The general idea is having these tokens: Soldier, Weapon, WeaponGem and Background.
// Weapon and Background can be equipped into Soldier. WeaponGem can be equipped into Weapon
// All use a single catalog.
// Soldier will use a single enumerated fixed asset for simplicity
// Weapon will have 2 assets per weapon, one for full view, one for equipping
// Background will have a single asset for each, it can be used as full view and to equip
// Weapon Gems will have 2 enumerated assets, one for full view, one for equipping.
describe("EquippableTokenMock with Slots", async () => {
let catalog: CatalogMock;
let soldier: EquippableTokenMock;
let weapon: EquippableTokenMock;
let weaponGem: EquippableTokenMock;
let background: EquippableTokenMock;
let view: EquipRenderUtils;
let addrs: SignerWithAddress[];
beforeEach(async function () {
[, ...addrs] = await ethers.getSigners();
({ catalog, soldier, weapon, weaponGem, background, view } =
await loadFixture(slotsFixture));
});
it("can support IERC6220", async function () {
expect(await soldier.supportsInterface("0x28bc9ae4")).to.equal(true);
});
describe("Validations", async function () {
it("can validate equips of weapons into snakeSoldiers", async function () {
// This asset is not equippable
expect(
await weapon.canTokenBeEquippedWithAssetIntoSlot(
soldier.address,
weaponsIds[0],
weaponAssetsFull[0],
partIdForWeapon
)
).to.eql(false);
// This asset is equippable into weapon part
expect(
await weapon.canTokenBeEquippedWithAssetIntoSlot(
soldier.address,
weaponsIds[0],
weaponAssetsEquip[0],
partIdForWeapon
)
).to.eql(true);
// This asset is NOT equippable into weapon gem part
expect(
await weapon.canTokenBeEquippedWithAssetIntoSlot(
soldier.address,
weaponsIds[0],
weaponAssetsEquip[0],
partIdForWeaponGem
)
).to.eql(false);
});
it("can validate equips of weapon gems into weapons", async function () {
// This asset is not equippable
expect(
await weaponGem.canTokenBeEquippedWithAssetIntoSlot(
weapon.address,
weaponGemsIds[0],
weaponGemAssetFull,
partIdForWeaponGem
)
).to.eql(false);
// This asset is equippable into weapon gem slot
expect(
await weaponGem.canTokenBeEquippedWithAssetIntoSlot(
weapon.address,
weaponGemsIds[0],
weaponGemAssetEquip,
partIdForWeaponGem
)
).to.eql(true);
// This asset is NOT equippable into background slot
expect(
await weaponGem.canTokenBeEquippedWithAssetIntoSlot(
weapon.address,
weaponGemsIds[0],
weaponGemAssetEquip,
partIdForBackground
)
).to.eql(false);
});
it("can validate equips of backgrounds into snakeSoldiers", async function () {
// This asset is equippable into background slot
expect(
await background.canTokenBeEquippedWithAssetIntoSlot(
soldier.address,
backgroundsIds[0],
backgroundAssetId,
partIdForBackground
)
).to.eql(true);
// This asset is NOT equippable into weapon slot
expect(
await background.canTokenBeEquippedWithAssetIntoSlot(
soldier.address,
backgroundsIds[0],
backgroundAssetId,
partIdForWeapon
)
).to.eql(false);
});
});
describe("Equip", async function () {
it("can equip weapon", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await equipWeaponAndCheckFromAddress(
soldierOwner,
childIndex,
weaponResId
);
});
it("can equip weapon if approved", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const approved = addrs[1];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(soldierOwner)
.approve(approved.address, snakeSoldiersIds[0]);
await equipWeaponAndCheckFromAddress(approved, childIndex, weaponResId);
});
it("can equip weapon if approved for all", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const approved = addrs[1];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(soldierOwner)
.setApprovalForAll(approved.address, true);
await equipWeaponAndCheckFromAddress(approved, childIndex, weaponResId);
});
it("can equip weapon and background", async function () {
// Weapon is child on index 0, background on index 1
const weaponChildIndex = 0;
const backgroundChildIndex = 1;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
weaponChildIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
backgroundChildIndex,
soldierResId,
partIdForBackground,
backgroundAssetId,
]);
const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)];
const expectedEquips = [
[bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weapon.address],
[
bn(soldierResId),
bn(backgroundAssetId),
bn(backgroundsIds[0]),
background.address,
],
];
expect(
await view.getEquipped(
soldier.address,
snakeSoldiersIds[0],
soldierResId
)
).to.eql([expectedSlots, expectedEquips]);
// Children are marked as equipped:
expect(
await soldier.isChildEquipped(
snakeSoldiersIds[0],
weapon.address,
weaponsIds[0]
)
).to.eql(true);
expect(
await soldier.isChildEquipped(
snakeSoldiersIds[0],
background.address,
backgroundsIds[0]
)
).to.eql(true);
});
it("cannot equip non existing child in slot (weapon in background)", async function () {
// Weapon is child on index 0, background on index 1
const badChildIndex = 3;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await expect(
soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
badChildIndex,
soldierResId,
partIdForWeapon,
weaponResId,
])
).to.be.reverted; // Bad index
});
it("cannot set a valid equippable group with id 0", async function () {
const equippableGroupId = 0;
// The malicious child indicates it can be equipped into soldier:
await expect(
weaponGem.setValidParentForEquippableGroup(
equippableGroupId,
soldier.address,
partIdForWeaponGem
)
).to.be.revertedWithCustomError(weaponGem, "IdZeroForbidden");
});
it("cannot set a valid equippable group with part id 0", async function () {
const equippableGroupId = 1;
const partId = 0;
// The malicious child indicates it can be equipped into soldier:
await expect(
weaponGem.setValidParentForEquippableGroup(
equippableGroupId,
soldier.address,
partId
)
).to.be.revertedWithCustomError(weaponGem, "IdZeroForbidden");
});
it("cannot equip into a slot not set on the parent asset (gem into soldier)", async function () {
const soldierOwner = addrs[0];
const soldierId = snakeSoldiersIds[0];
const childIndex = 2;
const newWeaponGemId = await nestMint(
weaponGem,
soldier.address,
soldierId
);
await soldier
.connect(soldierOwner)
.acceptChild(soldierId, 0, weaponGem.address, newWeaponGemId);
// Add assets to weapon
await weaponGem.addAssetToToken(newWeaponGemId, weaponGemAssetFull, 0);
await weaponGem.addAssetToToken(newWeaponGemId, weaponGemAssetEquip, 0);
await weaponGem
.connect(soldierOwner)
.acceptAsset(newWeaponGemId, 0, weaponGemAssetFull);
await weaponGem
.connect(soldierOwner)
.acceptAsset(newWeaponGemId, 0, weaponGemAssetEquip);
// The malicious child indicates it can be equipped into soldier:
await weaponGem.setValidParentForEquippableGroup(
1, // equippableGroupId for gems
soldier.address,
partIdForWeaponGem
);
// Weapon is child on index 0, background on index 1
await expect(
soldier
.connect(addrs[0])
.equip([
soldierId,
childIndex,
soldierResId,
partIdForWeaponGem,
weaponGemAssetEquip,
])
).to.be.revertedWithCustomError(soldier, "TargetAssetCannotReceiveSlot");
});
it("cannot equip wrong child in slot (weapon in background)", async function () {
// Weapon is child on index 0, background on index 1
const backgroundChildIndex = 1;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await expect(
soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
backgroundChildIndex,
soldierResId,
partIdForWeapon,
weaponResId,
])
).to.be.revertedWithCustomError(
soldier,
"TokenCannotBeEquippedWithAssetIntoSlot"
);
});
it("cannot equip child in wrong slot (weapon in background)", async function () {
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await expect(
soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForBackground,
weaponResId,
])
).to.be.revertedWithCustomError(
soldier,
"TokenCannotBeEquippedWithAssetIntoSlot"
);
});
it("cannot equip child with wrong asset (weapon in background)", async function () {
const childIndex = 0;
await expect(
soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
backgroundAssetId,
])
).to.be.revertedWithCustomError(
soldier,
"TokenCannotBeEquippedWithAssetIntoSlot"
);
});
it("cannot equip if not owner", async function () {
// Weapon is child on index 0, background on index 1
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await expect(
soldier
.connect(addrs[1]) // Owner is addrs[0]
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
])
).to.be.revertedWithCustomError(soldier, "ERC721NotApprovedOrOwner");
});
it("cannot equip 2 children into the same slot", async function () {
// Weapon is child on index 0, background on index 1
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
const weaponAssetIndex = 3;
await mintWeaponToSoldier(
addrs[0],
snakeSoldiersIds[0],
weaponAssetIndex
);
const newWeaponChildIndex = 2;
const newWeaponResId = weaponAssetsEquip[weaponAssetIndex];
await expect(
soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
newWeaponChildIndex,
soldierResId,
partIdForWeapon,
newWeaponResId,
])
).to.be.revertedWithCustomError(soldier, "SlotAlreadyUsed");
});
it("cannot equip if not intented on catalog", async function () {
// Weapon is child on index 0, background on index 1
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
// Remove equippable addresses for part.
await catalog.resetEquippableAddresses(partIdForWeapon);
await expect(
soldier
.connect(addrs[0]) // Owner is addrs[0]
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
])
).to.be.revertedWithCustomError(
soldier,
"EquippableEquipNotAllowedByCatalog"
);
});
});
describe("Unequip", async function () {
it("can unequip", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(soldierOwner)
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await unequipWeaponAndCheckFromAddress(soldierOwner);
});
it("can unequip if approved", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
const approved = addrs[1];
await soldier
.connect(soldierOwner)
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await soldier
.connect(soldierOwner)
.approve(approved.address, snakeSoldiersIds[0]);
await unequipWeaponAndCheckFromAddress(approved);
});
it("can unequip if approved for all", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
const approved = addrs[1];
await soldier
.connect(soldierOwner)
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await soldier
.connect(soldierOwner)
.setApprovalForAll(approved.address, true);
await unequipWeaponAndCheckFromAddress(approved);
});
it("cannot unequip if not equipped", async function () {
await expect(
soldier
.connect(addrs[0])
.unequip(snakeSoldiersIds[0], soldierResId, partIdForWeapon)
).to.be.revertedWithCustomError(soldier, "NotEquipped");
});
it("cannot unequip if not owner", async function () {
// Weapon is child on index 0, background on index 1
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await expect(
soldier
.connect(addrs[1])
.unequip(snakeSoldiersIds[0], soldierResId, partIdForWeapon)
).to.be.revertedWithCustomError(soldier, "ERC721NotApprovedOrOwner");
});
});
describe("Transfer equipped", async function () {
it("can unequip and transfer child", async function () {
// Weapon is child on index 0, background on index 1
const soldierOwner = addrs[0];
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(soldierOwner)
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await unequipWeaponAndCheckFromAddress(soldierOwner);
await soldier
.connect(soldierOwner)
.transferChild(
snakeSoldiersIds[0],
soldierOwner.address,
0,
childIndex,
weapon.address,
weaponsIds[0],
false,
"0x"
);
});
it("child transfer fails if child is equipped", async function () {
const soldierOwner = addrs[0];
// Weapon is child on index 0
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
await expect(
soldier
.connect(soldierOwner)
.transferChild(
snakeSoldiersIds[0],
soldierOwner.address,
0,
childIndex,
weapon.address,
weaponsIds[0],
false,
"0x"
)
).to.be.revertedWithCustomError(weapon, "MustUnequipFirst");
});
});
describe("Compose", async function () {
it("can compose equippables for soldier", async function () {
const childIndex = 0;
const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon
await soldier
.connect(addrs[0])
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
]);
const expectedFixedParts = [
[
bn(partIdForBody), // partId
1, // z
"genericBody.png", // metadataURI
],
];
const expectedSlotParts = [
[
bn(partIdForWeapon), // partId
bn(weaponAssetsEquip[0]), // childAssetId
2, // z
weapon.address, // childAddress
bn(weaponsIds[0]), // childTokenId
"ipfs:weapon/equip/5", // childAssetMetadata
"", // partMetadata
],
[
// Nothing on equipped on background slot:
bn(partIdForBackground), // partId
bn(0), // childAssetId
0, // z
ethers.constants.AddressZero, // childAddress
bn(0), // childTokenId
"", // childAssetMetadata
"noBackground.png", // partMetadata
],
];
const allAssets = await view.composeEquippables(
soldier.address,
snakeSoldiersIds[0],
soldierResId
);
expect(allAssets).to.eql([
"ipfs:soldier/", // metadataURI
bn(0), // equippableGroupId
catalog.address, // catalogAddress
expectedFixedParts,
expectedSlotParts,
]);
});
it("can compose equippables for simple asset", async function () {
const allAssets = await view.composeEquippables(
background.address,
backgroundsIds[0],
backgroundAssetId
);
expect(allAssets).to.eql([
"ipfs:background/", // metadataURI
bn(1), // equippableGroupId
catalog.address, // catalogAddress,
[],
[],
]);
});
it("cannot compose equippables for soldier with not associated asset", async function () {
const wrongResId = weaponAssetsEquip[1];
await expect(
view.composeEquippables(weapon.address, weaponsIds[0], wrongResId)
).to.be.revertedWithCustomError(weapon, "TokenDoesNotHaveAsset");
});
});
async function equipWeaponAndCheckFromAddress(
from: SignerWithAddress,
childIndex: number,
weaponResId: number
): Promise<void> {
await expect(
soldier
.connect(from)
.equip([
snakeSoldiersIds[0],
childIndex,
soldierResId,
partIdForWeapon,
weaponResId,
])
)
.to.emit(soldier, "ChildAssetEquipped")
.withArgs(
snakeSoldiersIds[0],
soldierResId,
partIdForWeapon,
weaponsIds[0],
weapon.address,
weaponAssetsEquip[0]
);
// All part slots are included on the response:
const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)];
// If a slot has nothing equipped, it returns an empty equip:
const expectedEquips = [
[bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weapon.address],
[bn(0), bn(0), bn(0), ethers.constants.AddressZero],
];
expect(
await view.getEquipped(soldier.address, snakeSoldiersIds[0], soldierResId)
).to.eql([expectedSlots, expectedEquips]);
// Child is marked as equipped:
expect(
await soldier.isChildEquipped(
snakeSoldiersIds[0],
weapon.address,
weaponsIds[0]
)
).to.eql(true);
}
async function unequipWeaponAndCheckFromAddress(
from: SignerWithAddress
): Promise<void> {
await expect(
soldier
.connect(from)
.unequip(snakeSoldiersIds[0], soldierResId, partIdForWeapon)
)
.to.emit(soldier, "ChildAssetUnequipped")
.withArgs(
snakeSoldiersIds[0],
soldierResId,
partIdForWeapon,
weaponsIds[0],
weapon.address,
weaponAssetsEquip[0]
);
const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)];
// If a slot has nothing equipped, it returns an empty equip:
const expectedEquips = [
[bn(0), bn(0), bn(0), ethers.constants.AddressZero],
[bn(0), bn(0), bn(0), ethers.constants.AddressZero],
];
expect(
await view.getEquipped(soldier.address, snakeSoldiersIds[0], soldierResId)
).to.eql([expectedSlots, expectedEquips]);
// Child is marked as not equipped:
expect(
await soldier.isChildEquipped(
snakeSoldiersIds[0],
weapon.address,
weaponsIds[0]
)
).to.eql(false);
}
async function mintWeaponToSoldier(
soldierOwner: SignerWithAddress,
soldierId: number,
assetIndex: number
): Promise<number> {
// Mint another weapon to the soldier and accept it
const newWeaponId = await nestMint(weapon, soldier.address, soldierId);
await soldier
.connect(soldierOwner)
.acceptChild(soldierId, 0, weapon.address, newWeaponId);
// Add assets to weapon
await weapon.addAssetToToken(newWeaponId, weaponAssetsFull[assetIndex], 0);
await weapon.addAssetToToken(newWeaponId, weaponAssetsEquip[assetIndex], 0);
await weapon
.connect(soldierOwner)
.acceptAsset(newWeaponId, 0, weaponAssetsFull[assetIndex]);
await weapon
.connect(soldierOwner)
.acceptAsset(newWeaponId, 0, weaponAssetsEquip[assetIndex]);
return newWeaponId;
}
});
function bn(x: number): BigNumber {
return BigNumber.from(x);
}