@broxus/tip4
Version:
Set of ready-to-use tvm nft token contracts following tip4 standard
490 lines (417 loc) • 16.9 kB
text/typescript
import { Address, Contract, toNano, zeroAddress } from 'locklift';
import { expect } from 'chai';
import { BigNumber } from 'bignumber.js';
import JsonTemplate from '../metadata-template.json';
import { CollectionAbi, NftAbi, CallbacksTestAbi } from '../build/factorySource';
describe('NFT', () => {
let owner: Address;
let user: Address;
let collection: Contract<CollectionAbi>;
let nft: Contract<NftAbi>;
let callbacks: Contract<CallbacksTestAbi>;
before('deploy contracts', async () => {
await locklift.deployments.fixture();
owner = locklift.deployments.getAccount('OwnerWallet').account.address;
user = locklift.deployments.getAccount('UserWallet').account.address;
collection = locklift.deployments.getContract<CollectionAbi>('Collection');
nft = locklift.deployments.getContract<NftAbi>('Nft');
callbacks = locklift.deployments.getContract<CallbacksTestAbi>('CallbacksTest');
});
describe('tip-4.1', () => {
it('should return NFT info', async () => {
const info = await nft.methods
.getInfo({ answerId: 0 })
.call();
expect(info.id).to.be.equal('0');
expect(info.collection.toString()).to.be.equal(collection.address.toString());
expect(info.owner.toString()).to.be.equal(user.toString());
return expect(info.manager.toString()).to.be.equal(user.toString());
});
describe('ownership changes from non-manager and non-owner of NFT', () => {
it('should throw SENDER_IS_NOT_MANAGER for changeManager', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.changeManager({ newManager: owner, sendGasTo: owner, callbacks: [] })
.send({ from: owner, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
it('should throw SENDER_IS_NOT_MANAGER for changeOwner', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.changeOwner({ newOwner: owner, sendGasTo: owner, callbacks: [] })
.send({ from: owner, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
it('should throw SENDER_IS_NOT_MANAGER for transfer', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.transfer({ to: owner, sendGasTo: owner, callbacks: [] })
.send({ from: owner, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
});
describe('change NFT manager', () => {
it('should change manager to "owner"', async () => {
await locklift.transactions.waitFinalized(
nft.methods
.changeManager({ newManager: owner, sendGasTo: user, callbacks: [] })
.send({ from: user, amount: toNano(1) }),
);
const info = await nft.methods
.getInfo({ answerId: 0 })
.call();
expect(info.owner.toString()).to.be.equal(user.toString());
return expect(info.manager.toString()).to.be.equal(owner.toString());
});
})
describe('ownership changes from owner of NFT', () => {
it('should throw SENDER_IS_NOT_MANAGER for changeManager from user', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.changeManager({ newManager: user, sendGasTo: user, callbacks: [] })
.send({ from: user, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
it('should throw SENDER_IS_NOT_MANAGER for changeOwner from user', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.changeOwner({ newOwner: user, sendGasTo: user, callbacks: [] })
.send({ from: user, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
it('should throw SENDER_IS_NOT_MANAGER for transfer from user', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.transfer({ to: user, sendGasTo: user, callbacks: [] })
.send({ from: user, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
});
describe('ownership changes from manager of NFT', () => {
it('should change owner to "owner"', async () => {
await locklift.transactions.waitFinalized(
nft.methods
.changeOwner({ newOwner: owner, sendGasTo: owner, callbacks: [] })
.send({ from: owner, amount: toNano(2) })
);
const info = await nft.methods
.getInfo({ answerId: 0 })
.call();
expect(info.owner.toString()).to.be.equal(owner.toString());
return expect(info.manager.toString()).to.be.equal(owner.toString());
});
it('should transfer NFT to "user"', async () => {
await locklift.transactions.waitFinalized(
nft.methods
.transfer({ to: user, sendGasTo: owner, callbacks: [] })
.send({ from: owner, amount: toNano(2) })
);
const info = await nft.methods
.getInfo({ answerId: 0 })
.call();
expect(info.owner.toString()).to.be.equal(user.toString());
return expect(info.manager.toString()).to.be.equal(user.toString());
});
});
});
describe('tip-4.2', () => {
it('should return JSON of NFT', async () => {
const nftJson = await nft.methods
.getJson({ answerId: 0 })
.call()
.then((r) => r.json);
return expect(nftJson).to.be.equal(JSON.stringify(JsonTemplate.nfts[0]));
});
});
describe('tip-4.3', () => {
it('should return Index code and its hash', async () => {
const nftIndexCode = await nft.methods
.indexCode({ answerId: 0 })
.call()
.then((r) => r.code);
const nftIndexCodeHash = await nft.methods
.indexCodeHash({ answerId: 0 })
.call()
.then((r) => new BigNumber(r.hash));
const IndexArtifacts = locklift.factory.getContractArtifacts('Index');
expect(nftIndexCodeHash.isEqualTo(IndexArtifacts.codeHash, 16)).to.be.true;
return expect(nftIndexCode).to.be.equal(IndexArtifacts.code);
});
it('should return valid address for Index by owner', async () => {
const index = await nft.methods
.resolveIndex({ answerId: 0, collection: zeroAddress, owner: user })
.call()
.then((r) => locklift.factory.getDeployedContract('Index', r.index));
const indexInfo = await index.methods
.getInfo({ answerId: 0 })
.call();
expect(indexInfo.collection.toString()).to.be.equal(collection.address.toString());
expect(indexInfo.owner.toString()).to.be.equal(user.toString());
return expect(indexInfo.nft.toString()).to.be.equal(nft.address.toString());
});
it('should return valid address for Index by collection and owner', async () => {
const index = await nft.methods
.resolveIndex({ answerId: 0, collection: collection.address, owner: user })
.call()
.then((r) => locklift.factory.getDeployedContract('Index', r.index));
const indexInfo = await index.methods
.getInfo({ answerId: 0 })
.call();
expect(indexInfo.collection.toString()).to.be.equal(collection.address.toString());
expect(indexInfo.owner.toString()).to.be.equal(user.toString());
return expect(indexInfo.nft.toString()).to.be.equal(nft.address.toString());
});
it('should change indexes after transfer', async () => {
const indexByUser = await nft.methods
.resolveIndex({ answerId: 0, collection: zeroAddress, owner: user })
.call()
.then((r) => r.index);
const indexByUserAndCollection = await nft.methods
.resolveIndex({ answerId: 0, collection: collection.address, owner: user })
.call()
.then((r) => r.index);
await locklift.transactions.waitFinalized(
nft.methods
.transfer({ to: owner, sendGasTo: user, callbacks: [] })
.send({ from: user, amount: toNano(2) })
);
const isIndexByUserDeployed = await locklift.provider
.getFullContractState({ address: indexByUser })
.then((r) => !!r.state?.isDeployed);
const isIndexByUserAndCollectionDeployed = await locklift.provider
.getFullContractState({ address: indexByUserAndCollection })
.then((r) => !!r.state?.isDeployed);
expect(isIndexByUserDeployed).to.be.false;
expect(isIndexByUserAndCollectionDeployed).to.be.false;
const indexByUserAfter = await nft.methods
.resolveIndex({ answerId: 0, collection: zeroAddress, owner: owner })
.call()
.then((r) => r.index);
const indexByUserAndCollectionAfter = await nft.methods
.resolveIndex({ answerId: 0, collection: collection.address, owner: owner })
.call()
.then((r) => r.index);
const isIndexByUserAfterDeployed = await locklift.provider
.getFullContractState({ address: indexByUserAfter })
.then((r) => !!r.state?.isDeployed);
const isIndexByUserAndCollectionAfterDeployed = await locklift.provider
.getFullContractState({ address: indexByUserAndCollectionAfter })
.then((r) => !!r.state?.isDeployed);
expect(isIndexByUserAfterDeployed).to.be.true;
return expect(isIndexByUserAndCollectionAfterDeployed).to.be.true;
});
});
describe('tip-6', () => {
it('should return true for tip-4.1 interface', async () => {
const isSupported = await nft.methods
.supportsInterface({
answerId: 0,
interfaceID: new BigNumber('0x78084f7e').toString(10)
})
.call()
.then((r) => r.value0);
return expect(isSupported).to.be.true;
});
it('should return true for tip-4.2 interface', async () => {
const isSupported = await nft.methods
.supportsInterface({
answerId: 0,
interfaceID: new BigNumber('0x24d7d5f5').toString(10)
})
.call()
.then((r) => r.value0);
return expect(isSupported).to.be.true;
});
it('should return true for tip-4.3 interface', async () => {
const isSupported = await nft.methods
.supportsInterface({
answerId: 0,
interfaceID: new BigNumber('0x4df6250b').toString(10)
})
.call()
.then((r) => r.value0);
return expect(isSupported).to.be.true;
});
it('should return true for tip-6 interface', async () => {
const isSupported = await nft.methods
.supportsInterface({
answerId: 0,
interfaceID: new BigNumber('0x3204ec29').toString(10)
})
.call()
.then((r) => r.value0);
return expect(isSupported).to.be.true;
});
it('should return false for unknown interface', async () => {
const isSupported = await nft.methods
.supportsInterface({
answerId: 0,
interfaceID: new BigNumber('0x3204ec25').toString(10)
})
.call()
.then((r) => r.value0);
return expect(isSupported).to.be.false;
});
});
describe('burn', () => {
it('should throw SENDER_IS_NOT_MANAGER for burn from non-manager', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.burn({ sendGasTo: user, callbackTo: zeroAddress, callbackPayload: '' })
.send({ from: user, amount: toNano(1), bounce: true }),
{ allowedCodes: { compute: [103] } },
);
return expect(traceTree).to.have.error(103);
});
it('should burn NFT and destroy its indexes', async () => {
const indexByUser = await nft.methods
.resolveIndex({ answerId: 0, collection: zeroAddress, owner: owner })
.call()
.then((r) => r.index);
const indexByUserAndCollection = await nft.methods
.resolveIndex({ answerId: 0, collection: collection.address, owner: owner })
.call()
.then((r) => r.index);
const { traceTree } = await locklift.tracing.trace(
nft.methods
.burn({ sendGasTo: owner, callbackTo: zeroAddress, callbackPayload: '' })
.send({ from: owner, amount: toNano(1) })
);
const isIndexByUserDeployed = await locklift.provider
.getFullContractState({ address: indexByUser })
.then((r) => !!r.state?.isDeployed);
const isIndexByUserAndCollectionDeployed = await locklift.provider
.getFullContractState({ address: indexByUserAndCollection })
.then((r) => !!r.state?.isDeployed);
const isNftDeployed = await nft
.getFullState()
.then((r) => !!r.state?.isDeployed);
expect(isIndexByUserDeployed).to.be.false;
expect(isIndexByUserAndCollectionDeployed).to.be.false;
expect(isNftDeployed).to.be.false;
return expect(traceTree)
.to.call('acceptNftBurn')
.count(1)
.and.to.emit('NftBurned')
.count(1)
.withNamedArgs({
id: '0',
nft: nft.address,
owner: owner,
manager: owner,
});
});
});
describe('callbacks', () => {
it('mint new NFT', async () => {
const { traceTree } = await locklift.tracing.trace(
collection.methods
.mintNft({ _json: JSON.stringify(JsonTemplate.nfts[0]), _owner: user })
.send({ from: owner, amount: toNano(3) }),
);
const nftAddress = traceTree?.findEventsForContract({ contract: collection, name: 'NftCreated' as const })[0];
nft = locklift.factory.getDeployedContract('Nft', nftAddress!.nft);
});
it('should change manager and receive callback', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.changeManager({
newManager: owner,
sendGasTo: user,
callbacks: [[callbacks.address, { value: toNano(0.1), payload: '' }]],
})
.send({ from: user, amount: toNano(1) }),
);
return expect(traceTree)
.to.call('onNftChangeManager')
.count(1)
.withNamedArgs({
id: '1',
owner: user,
oldManager: user,
newManager: owner,
collection: collection.address,
sendGasTo: user,
});
});
it('should change owner and receive callback', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.changeOwner({
newOwner: owner,
sendGasTo: owner,
callbacks: [[callbacks.address, { value: toNano(0.1), payload: '' }]],
})
.send({ from: owner, amount: toNano(1) }),
);
return expect(traceTree)
.to.call('onNftChangeOwner')
.count(1)
.withNamedArgs({
id: '1',
manager: owner,
oldOwner: user,
newOwner: owner,
collection: collection.address,
sendGasTo: owner,
});
});
it('should transfer NFT and receive callback', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.transfer({
to: user,
sendGasTo: owner,
callbacks: [[callbacks.address, { value: toNano(0.1), payload: '' }]],
})
.send({ from: owner, amount: toNano(1) }),
);
return expect(traceTree)
.to.call('onNftTransfer')
.count(1)
.withNamedArgs({
id: '1',
oldOwner: owner,
newOwner: user,
oldManager: owner,
newManager: user,
collection: collection.address,
gasReceiver: owner,
});
});
it('should burn NFT and receive callback', async () => {
const { traceTree } = await locklift.tracing.trace(
nft.methods
.burn({
sendGasTo: user,
callbackTo: callbacks.address,
callbackPayload: '',
})
.send({ from: user, amount: toNano(1) }),
);
return expect(traceTree)
.to.call('onAcceptNftBurn')
.count(1)
.withNamedArgs({
_collection: collection.address,
_id: '1',
_nft: nft.address,
_owner: user,
_manager: user,
_remainingGasTo: user,
});
});
});
});