mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
This PR adds a new non-fungible token sample using ERC721 functionalities. It includes javascript Chaincode and the README explaining how to mint and transfer a non-fungible token in the Fabric's test-network. Signed-off-by: Yuki Kondo <yuki.kondo.ob@hitachi.com>
338 lines
12 KiB
JavaScript
338 lines
12 KiB
JavaScript
/*
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { Context } = require('fabric-contract-api');
|
|
const { ChaincodeStub, ClientIdentity } = require('fabric-shim');
|
|
|
|
const { tokenERC721Contract } = require('..');
|
|
|
|
const chai = require('chai');
|
|
const chaiAsPromised = require('chai-as-promised');
|
|
const sinon = require('sinon');
|
|
const expect = chai.expect;
|
|
|
|
chai.should();
|
|
chai.use(chaiAsPromised);
|
|
|
|
class MockIterator {
|
|
constructor(data) {
|
|
this.array = data;
|
|
this.cur = 0;
|
|
}
|
|
next() {
|
|
if (this.cur < this.array.length) {
|
|
const value = this.array[this.cur];
|
|
this.cur++;
|
|
return Promise.resolve({ value: value });
|
|
} else {
|
|
return Promise.resolve({ done: true });
|
|
}
|
|
}
|
|
close() {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
|
|
describe('Chaincode', () => {
|
|
let sandbox;
|
|
let token;
|
|
let ctx;
|
|
let mockStub;
|
|
let mockClientIdentity;
|
|
|
|
beforeEach('Sandbox creation', () => {
|
|
sandbox = sinon.createSandbox();
|
|
token = new tokenERC721Contract('token-erc721');
|
|
|
|
ctx = sinon.createStubInstance(Context);
|
|
mockStub = sinon.createStubInstance(ChaincodeStub);
|
|
ctx.stub = mockStub;
|
|
mockClientIdentity = sinon.createStubInstance(ClientIdentity);
|
|
ctx.clientIdentity = mockClientIdentity;
|
|
});
|
|
|
|
afterEach('Sandbox restoration', () => {
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe('#BalanceOf', () => {
|
|
it('should work', async () => {
|
|
const mockResponse = [
|
|
{ key: 'balance_Alice_101', value: Buffer.from('\u0000') },
|
|
{ key: 'balance_Alice_102', value: Buffer.from('\u0000') }
|
|
];
|
|
mockStub.getStateByPartialCompositeKey.resolves(new MockIterator(mockResponse));
|
|
|
|
const response = await token.BalanceOf(ctx, 'Alice');
|
|
expect(response).to.equals(2);
|
|
});
|
|
});
|
|
|
|
describe('#OwnerOf', () => {
|
|
it('should work', async () => {
|
|
const nft = {
|
|
tokenId: 101,
|
|
owner: 'Alice'
|
|
};
|
|
sinon.stub(token, '_readNFT').resolves(nft);
|
|
|
|
const response = await token.OwnerOf(ctx, '101');
|
|
expect(response).to.equal('Alice');
|
|
});
|
|
});
|
|
|
|
describe('#TransferFrom', () => {
|
|
let currentNft;
|
|
let updatedNft;
|
|
|
|
beforeEach('Set up test parameters', () => {
|
|
currentNft = {
|
|
tokenId: 101,
|
|
owner: 'Alice',
|
|
approved: 'Charlie'
|
|
};
|
|
|
|
updatedNft = {
|
|
tokenId: 101,
|
|
owner: 'Bob',
|
|
approved: ''
|
|
};
|
|
|
|
sinon.stub(token, '_readNFT').resolves(currentNft);
|
|
mockStub.createCompositeKey.withArgs('nft', ['101']).returns('nft_101');
|
|
mockStub.createCompositeKey.withArgs('balance', ['Alice', '101']).returns('balance_Alice_101');
|
|
mockStub.createCompositeKey.withArgs('balance', ['Bob', '101']).returns('balance_Bob_101');
|
|
});
|
|
|
|
it('should work when a sender is the current owner', async () => {
|
|
mockClientIdentity.getID.returns('Alice');
|
|
sinon.stub(token, 'IsApprovedForAll').resolves(false);
|
|
|
|
const response = await token.TransferFrom(ctx, 'Alice', 'Bob', '101');
|
|
sinon.assert.calledWith(mockStub.putState, 'nft_101', Buffer.from(JSON.stringify(updatedNft)));
|
|
expect(response).to.equals(true);
|
|
});
|
|
|
|
it('should work when a sender is the approved client for this token', async () => {
|
|
mockClientIdentity.getID.returns('Charlie');
|
|
sinon.stub(token, 'IsApprovedForAll').resolves(false);
|
|
|
|
const response = await token.TransferFrom(ctx, 'Alice', 'Bob', '101');
|
|
sinon.assert.calledWith(mockStub.putState, 'nft_101', Buffer.from(JSON.stringify(updatedNft)));
|
|
expect(response).to.equals(true);
|
|
});
|
|
|
|
it('should work when a sender is an authorized operator', async () => {
|
|
mockClientIdentity.getID.returns('Dave');
|
|
sinon.stub(token, 'IsApprovedForAll').resolves(true);
|
|
|
|
const response = await token.TransferFrom(ctx, 'Alice', 'Bob', '101');
|
|
sinon.assert.calledWith(mockStub.putState, 'nft_101', Buffer.from(JSON.stringify(updatedNft)));
|
|
expect(response).to.equals(true);
|
|
});
|
|
|
|
it('should throw an error when a sender is invalid', async () => {
|
|
mockClientIdentity.getID.returns('Eve');
|
|
sinon.stub(token, 'IsApprovedForAll').resolves(false);
|
|
|
|
await expect(token.TransferFrom(ctx, 'Alice', 'Bob', '101'))
|
|
.to.be.rejectedWith(Error, 'The sender is not allowed to transfer the non-fungible token');
|
|
});
|
|
|
|
it('should throw an error when a current owner does not match', async () => {
|
|
mockClientIdentity.getID.returns('Dave');
|
|
sinon.stub(token, 'IsApprovedForAll').resolves(true);
|
|
|
|
await expect(token.TransferFrom(ctx, 'Charlie', 'Bob', '101'))
|
|
.to.be.rejectedWith(Error, 'The from is not the current owner.');
|
|
});
|
|
|
|
});
|
|
|
|
describe('#Approve', () => {
|
|
it('should work with the token owner', async () => {
|
|
mockClientIdentity.getID.returns('Alice');
|
|
const currentNft = {
|
|
tokenId: 101,
|
|
owner: 'Alice',
|
|
};
|
|
sinon.stub(token, '_readNFT').resolves(currentNft);
|
|
sinon.stub(token, 'IsApprovedForAll').resolves(false);
|
|
mockStub.createCompositeKey.withArgs('nft', ['101']).returns('nft_101');
|
|
|
|
const response = await token.Approve(ctx, 'Bob', '101');
|
|
const updatedNft = {
|
|
tokenId: 101,
|
|
owner: 'Alice',
|
|
approved: 'Bob'
|
|
};
|
|
sinon.assert.calledWith(mockStub.putState, 'nft_101', Buffer.from(JSON.stringify(updatedNft)));
|
|
expect(response).to.equals(true);
|
|
});
|
|
});
|
|
|
|
describe('#SetApprovalForAll', () => {
|
|
it('should work', async () => {
|
|
mockClientIdentity.getID.returns('Alice');
|
|
mockStub.createCompositeKey.withArgs('approval', ['Alice', 'Bob']).returns('approval_Alice_Bob');
|
|
|
|
const response = await token.SetApprovalForAll(ctx, 'Bob', true);
|
|
const approval = {
|
|
owner: 'Alice',
|
|
operator: 'Bob',
|
|
approved: true
|
|
};
|
|
sinon.assert.calledWith(mockStub.putState, 'approval_Alice_Bob', Buffer.from(JSON.stringify(approval)));
|
|
expect(response).to.equals(true);
|
|
});
|
|
});
|
|
|
|
describe('#GetApproved', () => {
|
|
it('should work', async () => {
|
|
const nft = {
|
|
tokenId: 101,
|
|
owner: 'Alice',
|
|
approved: 'Bob',
|
|
};
|
|
sinon.stub(token, '_readNFT').resolves(nft);
|
|
|
|
const response = await token.GetApproved(ctx, '101');
|
|
expect(response).to.equals('Bob');
|
|
});
|
|
});
|
|
|
|
describe('#IsApprovedForAll', () => {
|
|
it('should work', async () => {
|
|
mockStub.createCompositeKey.withArgs('approval', ['Alice', 'Bob']).returns('approval_Alice_Bob');
|
|
const approval = {
|
|
owner: 'Alice',
|
|
operator: 'Bob',
|
|
approved: true
|
|
};
|
|
mockStub.getState.withArgs('approval_Alice_Bob').resolves(Buffer.from(JSON.stringify(approval)));
|
|
|
|
const response = await token.IsApprovedForAll(ctx, 'Alice', 'Bob');
|
|
expect(response).to.equals(true);
|
|
});
|
|
});
|
|
|
|
describe('#Name', () => {
|
|
it('should work', async () => {
|
|
mockStub.getState.resolves('some state');
|
|
|
|
const response = await token.Name(ctx);
|
|
expect(response).to.equals('some state');
|
|
});
|
|
});
|
|
|
|
describe('#Symbol', () => {
|
|
it('should work', async () => {
|
|
mockStub.getState.resolves('some state');
|
|
|
|
const response = await token.Symbol(ctx);
|
|
expect(response).to.equals('some state');
|
|
});
|
|
});
|
|
|
|
describe('#TokenURI', () => {
|
|
it('should work', async () => {
|
|
const nft = {
|
|
tokenId: 101,
|
|
owner: 'Alice',
|
|
tokenURI: 'DummyURI'
|
|
};
|
|
sinon.stub(token, '_readNFT').resolves(nft);
|
|
|
|
const response = await token.TokenURI(ctx, '101');
|
|
expect(response).to.equal('DummyURI');
|
|
});
|
|
});
|
|
|
|
describe('#TotalSupply', () => {
|
|
it('should work', async () => {
|
|
const mockResponse = [
|
|
{ key: 'nft_101', value: Buffer.from(JSON.stringify({ tokenId: 101, owner: 'Alice' })) },
|
|
{ key: 'nft_102', value: Buffer.from(JSON.stringify({ tokenId: 102, owner: 'Bob' })) }
|
|
];
|
|
mockStub.getStateByPartialCompositeKey.resolves(new MockIterator(mockResponse));
|
|
|
|
const response = await token.TotalSupply(ctx);
|
|
expect(response).to.equals(2);
|
|
});
|
|
});
|
|
|
|
describe('#MintWithTokenURI', () => {
|
|
it('should work with a new token', async () => {
|
|
mockClientIdentity.getMSPID.returns('Org1MSP');
|
|
mockClientIdentity.getID.returns('Alice');
|
|
sinon.stub(token, '_nftExists').resolves(false);
|
|
mockStub.createCompositeKey.withArgs('nft', ['101']).returns('nft_101');
|
|
mockStub.createCompositeKey.withArgs('balance', ['Alice', '101']).returns('balance_Alice_101');
|
|
|
|
const response = await token.MintWithTokenURI(ctx, '101', 'DummyURI');
|
|
const nft = { tokenId: 101, owner: 'Alice', tokenURI: 'DummyURI'};
|
|
sinon.assert.calledWith(mockStub.putState.getCall(0), 'nft_101', Buffer.from(JSON.stringify(nft)));
|
|
sinon.assert.calledWith(mockStub.putState.getCall(1), 'balance_Alice_101', Buffer.from('\u0000'));
|
|
expect(response).to.deep.equal(nft);
|
|
});
|
|
|
|
it('should throw an error when a tokenId alreay exists', async () => {
|
|
mockClientIdentity.getMSPID.returns('Org1MSP');
|
|
mockClientIdentity.getID.returns('Alice');
|
|
sinon.stub(token, '_nftExists').resolves(true);
|
|
|
|
await expect(token.MintWithTokenURI(ctx, 'mytoken1', 'DummyURI'))
|
|
.to.be.rejectedWith(Error, 'The token mytoken1 is already minted.');
|
|
});
|
|
|
|
it('should throw an error when a tokenId is not an integer', async () => {
|
|
mockClientIdentity.getMSPID.returns('Org1MSP');
|
|
mockClientIdentity.getID.returns('Alice');
|
|
sinon.stub(token, '_nftExists').resolves(false);
|
|
|
|
await expect(token.MintWithTokenURI(ctx, 'mytoken1', 'DummyURI'))
|
|
.to.be.rejectedWith(Error, 'The tokenId mytoken1 is invalid. tokenId must be an integer');
|
|
});
|
|
|
|
});
|
|
|
|
describe('#Burn', () => {
|
|
it('should work', async () => {
|
|
mockClientIdentity.getID.returns('Bob');
|
|
|
|
const nft = {
|
|
tokenId: 101,
|
|
owner: 'Bob',
|
|
};
|
|
sinon.stub(token, '_readNFT').resolves(nft);
|
|
|
|
mockStub.createCompositeKey.withArgs('nft', ['101']).returns('nft_101');
|
|
mockStub.createCompositeKey.withArgs('balance', ['Bob', '101']).returns('balance_Bob_101');
|
|
|
|
const response = await token.Burn(ctx, '101');
|
|
sinon.assert.calledWith(mockStub.deleteState.getCall(0), 'nft_101');
|
|
sinon.assert.calledWith(mockStub.deleteState.getCall(1), 'balance_Bob_101');
|
|
expect(response).to.equals(true);
|
|
});
|
|
});
|
|
|
|
describe('#_readNFT', () => {
|
|
it('should work', async () => {
|
|
mockStub.createCompositeKey.returns('nft_101');
|
|
const nft = {
|
|
tokenId: 101,
|
|
owner: 'Alice',
|
|
approved: 'Bob',
|
|
tokenURI: 'DummyURI'
|
|
};
|
|
mockStub.getState.resolves(Buffer.from(JSON.stringify(nft)));
|
|
|
|
const response = await token._readNFT(ctx, '101');
|
|
expect(response).to.deep.equal(nft);
|
|
});
|
|
});
|
|
|
|
});
|