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

663 lines
19 KiB
TypeScript

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";
let addrs: SignerWithAddress[];
const partIdForHead1 = 1;
const partIdForHead2 = 2;
const partIdForHead3 = 3;
const partIdForBody1 = 4;
const partIdForBody2 = 5;
const partIdForHair1 = 6;
const partIdForHair2 = 7;
const partIdForHair3 = 8;
const partIdForMaskCatalog1 = 9;
const partIdForMaskCatalog2 = 10;
const partIdForMaskCatalog3 = 11;
const partIdForEars1 = 12;
const partIdForEars2 = 13;
const partIdForHorns1 = 14;
const partIdForHorns2 = 15;
const partIdForHorns3 = 16;
const partIdForMaskCatalogEquipped1 = 17;
const partIdForMaskCatalogEquipped2 = 18;
const partIdForMaskCatalogEquipped3 = 19;
const partIdForEarsEquipped1 = 20;
const partIdForEarsEquipped2 = 21;
const partIdForHornsEquipped1 = 22;
const partIdForHornsEquipped2 = 23;
const partIdForHornsEquipped3 = 24;
const partIdForMask = 25;
const uniqueNeons = 10;
const uniqueMasks = 4;
// Ids could be the same since they are different collections, but to avoid log problems we have them unique
const neons: number[] = [];
const masks: number[] = [];
const neonResIds = [100, 101, 102, 103, 104];
const maskAssetsFull = [1, 2, 3, 4]; // Must match the total of uniqueAssets
const maskAssetsEquip = [5, 6, 7, 8]; // Must match the total of uniqueAssets
const maskpableGroupId = 1; // Assets to equip will all use this
enum ItemType {
None,
Slot,
Fixed,
}
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 setupContextForParts(
catalog: CatalogMock,
neon: EquippableTokenMock,
mask: EquippableTokenMock
) {
[, ...addrs] = await ethers.getSigners();
await setupCatalog();
await mintNeons();
await mintMasks();
await addAssetsToNeon();
await addAssetsToMask();
async function setupCatalog(): Promise<void> {
const partForHead1 = {
itemType: ItemType.Fixed,
z: 1,
equippable: [],
metadataURI: "ipfs://head1.png",
};
const partForHead2 = {
itemType: ItemType.Fixed,
z: 1,
equippable: [],
metadataURI: "ipfs://head2.png",
};
const partForHead3 = {
itemType: ItemType.Fixed,
z: 1,
equippable: [],
metadataURI: "ipfs://head3.png",
};
const partForBody1 = {
itemType: ItemType.Fixed,
z: 1,
equippable: [],
metadataURI: "ipfs://body1.png",
};
const partForBody2 = {
itemType: ItemType.Fixed,
z: 1,
equippable: [],
metadataURI: "ipfs://body2.png",
};
const partForHair1 = {
itemType: ItemType.Fixed,
z: 2,
equippable: [],
metadataURI: "ipfs://hair1.png",
};
const partForHair2 = {
itemType: ItemType.Fixed,
z: 2,
equippable: [],
metadataURI: "ipfs://hair2.png",
};
const partForHair3 = {
itemType: ItemType.Fixed,
z: 2,
equippable: [],
metadataURI: "ipfs://hair3.png",
};
const partForMaskCatalog1 = {
itemType: ItemType.Fixed,
z: 3,
equippable: [],
metadataURI: "ipfs://maskCatalog1.png",
};
const partForMaskCatalog2 = {
itemType: ItemType.Fixed,
z: 3,
equippable: [],
metadataURI: "ipfs://maskCatalog2.png",
};
const partForMaskCatalog3 = {
itemType: ItemType.Fixed,
z: 3,
equippable: [],
metadataURI: "ipfs://maskCatalog3.png",
};
const partForEars1 = {
itemType: ItemType.Fixed,
z: 4,
equippable: [],
metadataURI: "ipfs://ears1.png",
};
const partForEars2 = {
itemType: ItemType.Fixed,
z: 4,
equippable: [],
metadataURI: "ipfs://ears2.png",
};
const partForHorns1 = {
itemType: ItemType.Fixed,
z: 5,
equippable: [],
metadataURI: "ipfs://horn1.png",
};
const partForHorns2 = {
itemType: ItemType.Fixed,
z: 5,
equippable: [],
metadataURI: "ipfs://horn2.png",
};
const partForHorns3 = {
itemType: ItemType.Fixed,
z: 5,
equippable: [],
metadataURI: "ipfs://horn3.png",
};
const partForMaskCatalogEquipped1 = {
itemType: ItemType.Fixed,
z: 3,
equippable: [],
metadataURI: "ipfs://maskCatalogEquipped1.png",
};
const partForMaskCatalogEquipped2 = {
itemType: ItemType.Fixed,
z: 3,
equippable: [],
metadataURI: "ipfs://maskCatalogEquipped2.png",
};
const partForMaskCatalogEquipped3 = {
itemType: ItemType.Fixed,
z: 3,
equippable: [],
metadataURI: "ipfs://maskCatalogEquipped3.png",
};
const partForEarsEquipped1 = {
itemType: ItemType.Fixed,
z: 4,
equippable: [],
metadataURI: "ipfs://earsEquipped1.png",
};
const partForEarsEquipped2 = {
itemType: ItemType.Fixed,
z: 4,
equippable: [],
metadataURI: "ipfs://earsEquipped2.png",
};
const partForHornsEquipped1 = {
itemType: ItemType.Fixed,
z: 5,
equippable: [],
metadataURI: "ipfs://hornEquipped1.png",
};
const partForHornsEquipped2 = {
itemType: ItemType.Fixed,
z: 5,
equippable: [],
metadataURI: "ipfs://hornEquipped2.png",
};
const partForHornsEquipped3 = {
itemType: ItemType.Fixed,
z: 5,
equippable: [],
metadataURI: "ipfs://hornEquipped3.png",
};
const partForMask = {
itemType: ItemType.Slot,
z: 2,
equippable: [mask.address],
metadataURI: "",
};
await catalog.addPartList([
{ partId: partIdForHead1, part: partForHead1 },
{ partId: partIdForHead2, part: partForHead2 },
{ partId: partIdForHead3, part: partForHead3 },
{ partId: partIdForBody1, part: partForBody1 },
{ partId: partIdForBody2, part: partForBody2 },
{ partId: partIdForHair1, part: partForHair1 },
{ partId: partIdForHair2, part: partForHair2 },
{ partId: partIdForHair3, part: partForHair3 },
{ partId: partIdForMaskCatalog1, part: partForMaskCatalog1 },
{ partId: partIdForMaskCatalog2, part: partForMaskCatalog2 },
{ partId: partIdForMaskCatalog3, part: partForMaskCatalog3 },
{ partId: partIdForEars1, part: partForEars1 },
{ partId: partIdForEars2, part: partForEars2 },
{ partId: partIdForHorns1, part: partForHorns1 },
{ partId: partIdForHorns2, part: partForHorns2 },
{ partId: partIdForHorns3, part: partForHorns3 },
{
partId: partIdForMaskCatalogEquipped1,
part: partForMaskCatalogEquipped1,
},
{
partId: partIdForMaskCatalogEquipped2,
part: partForMaskCatalogEquipped2,
},
{
partId: partIdForMaskCatalogEquipped3,
part: partForMaskCatalogEquipped3,
},
{ partId: partIdForEarsEquipped1, part: partForEarsEquipped1 },
{ partId: partIdForEarsEquipped2, part: partForEarsEquipped2 },
{ partId: partIdForHornsEquipped1, part: partForHornsEquipped1 },
{ partId: partIdForHornsEquipped2, part: partForHornsEquipped2 },
{ partId: partIdForHornsEquipped3, part: partForHornsEquipped3 },
{ partId: partIdForMask, part: partForMask },
]);
}
async function mintNeons(): Promise<void> {
// This array is reused, so we "empty" it before
neons.length = 0;
// Using only first 3 addresses to mint
for (let i = 0; i < uniqueNeons; i++) {
const newId = await mint(neon, addrs[i % 3].address);
neons.push(newId);
}
}
async function mintMasks(): Promise<void> {
// This array is reused, so we "empty" it before
masks.length = 0;
// Mint one weapon to neon
for (let i = 0; i < uniqueNeons; i++) {
const newId = await nestMint(mask, neon.address, neons[i]);
masks.push(newId);
await neon
.connect(addrs[i % 3])
.acceptChild(neons[i], 0, mask.address, newId);
}
}
async function addAssetsToNeon(): Promise<void> {
await neon.addEquippableAssetEntry(
neonResIds[0],
0,
catalog.address,
"ipfs:neonRes/1",
[partIdForHead1, partIdForBody1, partIdForHair1, partIdForMask]
);
await neon.addEquippableAssetEntry(
neonResIds[1],
0,
catalog.address,
"ipfs:neonRes/2",
[partIdForHead2, partIdForBody2, partIdForHair2, partIdForMask]
);
await neon.addEquippableAssetEntry(
neonResIds[2],
0,
catalog.address,
"ipfs:neonRes/3",
[partIdForHead3, partIdForBody1, partIdForHair3, partIdForMask]
);
await neon.addEquippableAssetEntry(
neonResIds[3],
0,
catalog.address,
"ipfs:neonRes/4",
[partIdForHead1, partIdForBody2, partIdForHair2, partIdForMask]
);
await neon.addEquippableAssetEntry(
neonResIds[4],
0,
catalog.address,
"ipfs:neonRes/1",
[partIdForHead2, partIdForBody1, partIdForHair1, partIdForMask]
);
for (let i = 0; i < uniqueNeons; i++) {
await neon.addAssetToToken(
neons[i],
neonResIds[i % neonResIds.length],
0
);
await neon
.connect(addrs[i % 3])
.acceptAsset(neons[i], 0, neonResIds[i % neonResIds.length]);
}
}
async function addAssetsToMask(): Promise<void> {
// Assets for full view, composed with fixed parts
await mask.addEquippableAssetEntry(
maskAssetsFull[0],
0, // Not meant to equip
catalog.address, // Not meant to equip, but catalog needed for parts
`ipfs:weapon/full/${maskAssetsFull[0]}`,
[partIdForMaskCatalog1, partIdForHorns1, partIdForEars1]
);
await mask.addEquippableAssetEntry(
maskAssetsFull[1],
0, // Not meant to equip
catalog.address, // Not meant to equip, but catalog needed for parts
`ipfs:weapon/full/${maskAssetsFull[1]}`,
[partIdForMaskCatalog2, partIdForHorns2, partIdForEars2]
);
await mask.addEquippableAssetEntry(
maskAssetsFull[2],
0, // Not meant to equip
catalog.address, // Not meant to equip, but catalog needed for parts
`ipfs:weapon/full/${maskAssetsFull[2]}`,
[partIdForMaskCatalog3, partIdForHorns1, partIdForEars2]
);
await mask.addEquippableAssetEntry(
maskAssetsFull[3],
0, // Not meant to equip
catalog.address, // Not meant to equip, but catalog needed for parts
`ipfs:weapon/full/${maskAssetsFull[3]}`,
[partIdForMaskCatalog2, partIdForHorns2, partIdForEars1]
);
// Assets for equipping view, also composed with fixed parts
await mask.addEquippableAssetEntry(
maskAssetsEquip[0],
maskpableGroupId,
catalog.address,
`ipfs:weapon/equip/${maskAssetsEquip[0]}`,
[partIdForMaskCatalog1, partIdForHorns1, partIdForEars1]
);
// Assets for equipping view, also composed with fixed parts
await mask.addEquippableAssetEntry(
maskAssetsEquip[1],
maskpableGroupId,
catalog.address,
`ipfs:weapon/equip/${maskAssetsEquip[1]}`,
[partIdForMaskCatalog2, partIdForHorns2, partIdForEars2]
);
// Assets for equipping view, also composed with fixed parts
await mask.addEquippableAssetEntry(
maskAssetsEquip[2],
maskpableGroupId,
catalog.address,
`ipfs:weapon/equip/${maskAssetsEquip[2]}`,
[partIdForMaskCatalog3, partIdForHorns1, partIdForEars2]
);
// Assets for equipping view, also composed with fixed parts
await mask.addEquippableAssetEntry(
maskAssetsEquip[3],
maskpableGroupId,
catalog.address,
`ipfs:weapon/equip/${maskAssetsEquip[3]}`,
[partIdForMaskCatalog2, partIdForHorns2, partIdForEars1]
);
// Can be equipped into neons
await mask.setValidParentForEquippableGroup(
maskpableGroupId,
neon.address,
partIdForMask
);
// 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 < masks.length; i++) {
await mask.addAssetToToken(masks[i], maskAssetsFull[i % uniqueMasks], 0);
await mask.addAssetToToken(masks[i], maskAssetsEquip[i % uniqueMasks], 0);
await mask
.connect(addrs[i % 3])
.acceptAsset(masks[i], 0, maskAssetsFull[i % uniqueMasks]);
await mask
.connect(addrs[i % 3])
.acceptAsset(masks[i], 0, maskAssetsEquip[i % uniqueMasks]);
}
}
}
async function partsFixture() {
const baseSymbol = "NCB";
const baseType = "mixed";
const baseFactory = await ethers.getContractFactory("CatalogMock");
const equipFactory = await ethers.getContractFactory("EquippableTokenMock");
const viewFactory = await ethers.getContractFactory("EquipRenderUtils");
// Catalog
const catalog = <CatalogMock>await baseFactory.deploy(baseSymbol, baseType);
await catalog.deployed();
// Neon token
const neon = <EquippableTokenMock>await equipFactory.deploy();
await neon.deployed();
// Weapon
const mask = <EquippableTokenMock>await equipFactory.deploy();
await mask.deployed();
// View
const view = <EquipRenderUtils>await viewFactory.deploy();
await view.deployed();
await setupContextForParts(catalog, neon, mask);
return { catalog, neon, mask, view };
}
// The general idea is having these tokens: Neon and Mask
// Masks can be equipped into Neons.
// All use a single catalog.
// Neon will use an asset per token, which uses fixed parts to compose the body
// Mask will have 2 assets per weapon, one for full view, one for equipping. Both are composed using fixed parts
describe("EquippableTokenMock with Parts", async () => {
let catalog: CatalogMock;
let neon: EquippableTokenMock;
let mask: EquippableTokenMock;
let view: EquipRenderUtils;
let addrs: SignerWithAddress[];
beforeEach(async function () {
[, ...addrs] = await ethers.getSigners();
({ catalog, neon, mask, view } = await loadFixture(partsFixture));
});
describe("Equip", async function () {
it("can equip weapon", async function () {
// Weapon is child on index 0, background on index 1
const childIndex = 0;
const weaponResId = maskAssetsEquip[0]; // This asset is assigned to weapon first weapon
await expect(
neon
.connect(addrs[0])
.equip([
neons[0],
childIndex,
neonResIds[0],
partIdForMask,
weaponResId,
])
)
.to.emit(neon, "ChildAssetEquipped")
.withArgs(
neons[0],
neonResIds[0],
partIdForMask,
masks[0],
mask.address,
weaponResId
);
// All part slots are included on the response:
const expectedSlots = [bn(partIdForMask)];
const expectedEquips = [
[bn(neonResIds[0]), bn(weaponResId), bn(masks[0]), mask.address],
];
expect(
await view.getEquipped(neon.address, neons[0], neonResIds[0])
).to.eql([expectedSlots, expectedEquips]);
// Child is marked as equipped:
expect(
await neon.isChildEquipped(neons[0], mask.address, masks[0])
).to.eql(true);
});
it("cannot equip non existing child in slot", async function () {
// Weapon is child on index 0
const badChildIndex = 3;
const weaponResId = maskAssetsEquip[0]; // This asset is assigned to weapon first weapon
await expect(
neon
.connect(addrs[0])
.equip([
neons[0],
badChildIndex,
neonResIds[0],
partIdForMask,
weaponResId,
])
).to.be.reverted; // Bad index
});
});
describe("Compose", async function () {
it("can compose all parts for neon", async function () {
const childIndex = 0;
const weaponResId = maskAssetsEquip[0]; // This asset is assigned to weapon first weapon
await neon
.connect(addrs[0])
.equip([
neons[0],
childIndex,
neonResIds[0],
partIdForMask,
weaponResId,
]);
const expectedFixedParts = [
[
bn(partIdForHead1), // partId
1, // z
"ipfs://head1.png", // metadataURI
],
[
bn(partIdForBody1), // partId
1, // z
"ipfs://body1.png", // metadataURI
],
[
bn(partIdForHair1), // partId
2, // z
"ipfs://hair1.png", // metadataURI
],
];
const expectedSlotParts = [
[
bn(partIdForMask), // partId
bn(maskAssetsEquip[0]), // childAssetId
2, // z
mask.address, // childAddress
bn(masks[0]), // childTokenId
"ipfs:weapon/equip/5", // childAssetMetadata
"", // partMetadata
],
];
const allAssets = await view.composeEquippables(
neon.address,
neons[0],
neonResIds[0]
);
expect(allAssets).to.eql([
"ipfs:neonRes/1", // metadataURI
bn(0), // equippableGroupId
catalog.address, // baseAddress,
expectedFixedParts,
expectedSlotParts,
]);
});
it("can compose all parts for mask", async function () {
const expectedFixedParts = [
[
bn(partIdForMaskCatalog1), // partId
3, // z
"ipfs://maskCatalog1.png", // metadataURI
],
[
bn(partIdForHorns1), // partId
5, // z
"ipfs://horn1.png", // metadataURI
],
[
bn(partIdForEars1), // partId
4, // z
"ipfs://ears1.png", // metadataURI
],
];
const allAssets = await view.composeEquippables(
mask.address,
masks[0],
maskAssetsEquip[0]
);
expect(allAssets).to.eql([
`ipfs:weapon/equip/${maskAssetsEquip[0]}`, // metadataURI
bn(maskpableGroupId), // equippableGroupId
catalog.address, // baseAddress
expectedFixedParts,
[],
]);
});
it("cannot compose equippables for neon with not associated asset", async function () {
const wrongResId = maskAssetsEquip[1];
await expect(
view.composeEquippables(mask.address, masks[0], wrongResId)
).to.be.revertedWithCustomError(mask, "TokenDoesNotHaveAsset");
});
it("cannot compose equippables for mask for asset with no catalog", async function () {
const noCatalogAssetId = 99;
await mask.addEquippableAssetEntry(
noCatalogAssetId,
0, // Not meant to equip
ethers.constants.AddressZero, // Not meant to equip
`ipfs:weapon/full/customAsset.png`,
[]
);
await mask.addAssetToToken(masks[0], noCatalogAssetId, 0);
await mask.connect(addrs[0]).acceptAsset(masks[0], 0, noCatalogAssetId);
await expect(
view.composeEquippables(mask.address, masks[0], noCatalogAssetId)
).to.be.revertedWithCustomError(view, "NotComposableAsset");
});
});
});
function bn(x: number): BigNumber {
return BigNumber.from(x);
}