mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 15:35:09 +00:00
464 lines
No EOL
18 KiB
JavaScript
464 lines
No EOL
18 KiB
JavaScript
/*
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { Contract } = require('fabric-contract-api');
|
|
|
|
// Define objectType names for prefix
|
|
const balancePrefix = 'balance';
|
|
const nftPrefix = 'nft';
|
|
const approvalPrefix = 'approval';
|
|
|
|
// Define key names for options
|
|
const nameKey = 'name';
|
|
const symbolKey = 'symbol';
|
|
|
|
class TokenERC721Contract extends Contract {
|
|
|
|
/**
|
|
* BalanceOf counts all non-fungible tokens assigned to an owner
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} owner An owner for whom to query the balance
|
|
* @returns {Number} The number of non-fungible tokens owned by the owner, possibly zero
|
|
*/
|
|
async BalanceOf(ctx, owner) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
// There is a key record for every non-fungible token in the format of balancePrefix.owner.tokenId.
|
|
// BalanceOf() queries for and counts all records matching balancePrefix.owner.*
|
|
const iterator = await ctx.stub.getStateByPartialCompositeKey(balancePrefix, [owner]);
|
|
|
|
// Count the number of returned composite keys
|
|
let balance = 0;
|
|
let result = await iterator.next();
|
|
while (!result.done) {
|
|
balance++;
|
|
result = await iterator.next();
|
|
}
|
|
return balance;
|
|
}
|
|
|
|
/**
|
|
* OwnerOf finds the owner of a non-fungible token
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} tokenId The identifier for a non-fungible token
|
|
* @returns {String} Return the owner of the non-fungible token
|
|
*/
|
|
async OwnerOf(ctx, tokenId) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const nft = await this._readNFT(ctx, tokenId);
|
|
const owner = nft.owner;
|
|
if (!owner) {
|
|
throw new Error('No owner is assigned to this token');
|
|
}
|
|
|
|
return owner;
|
|
}
|
|
|
|
/**
|
|
* TransferFrom transfers the ownership of a non-fungible token
|
|
* from one owner to another owner
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} from The current owner of the non-fungible token
|
|
* @param {String} to The new owner
|
|
* @param {String} tokenId the non-fungible token to transfer
|
|
* @returns {Boolean} Return whether the transfer was successful or not
|
|
*/
|
|
async TransferFrom(ctx, from, to, tokenId) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const sender = ctx.clientIdentity.getID();
|
|
|
|
const nft = await this._readNFT(ctx, tokenId);
|
|
|
|
// Check if the sender is the current owner, an authorized operator,
|
|
// or the approved client for this non-fungible token.
|
|
const owner = nft.owner;
|
|
const tokenApproval = nft.approved;
|
|
const operatorApproval = await this.IsApprovedForAll(ctx, owner, sender);
|
|
if (owner !== sender && tokenApproval !== sender && !operatorApproval) {
|
|
throw new Error('The sender is not allowed to transfer the non-fungible token');
|
|
}
|
|
|
|
// Check if `from` is the current owner
|
|
if (owner !== from) {
|
|
throw new Error('The from is not the current owner.');
|
|
}
|
|
|
|
// Clear the approved client for this non-fungible token
|
|
nft.approved = '';
|
|
|
|
// Overwrite a non-fungible token to assign a new owner.
|
|
nft.owner = to;
|
|
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
|
|
await ctx.stub.putState(nftKey, Buffer.from(JSON.stringify(nft)));
|
|
|
|
// Remove a composite key from the balance of the current owner
|
|
const balanceKeyFrom = ctx.stub.createCompositeKey(balancePrefix, [from, tokenId]);
|
|
await ctx.stub.deleteState(balanceKeyFrom);
|
|
|
|
// Save a composite key to count the balance of a new owner
|
|
const balanceKeyTo = ctx.stub.createCompositeKey(balancePrefix, [to, tokenId]);
|
|
await ctx.stub.putState(balanceKeyTo, Buffer.from('\u0000'));
|
|
|
|
// Emit the Transfer event
|
|
const tokenIdInt = parseInt(tokenId);
|
|
const transferEvent = { from: from, to: to, tokenId: tokenIdInt };
|
|
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Approve changes or reaffirms the approved client for a non-fungible token
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} approved The new approved client
|
|
* @param {String} tokenId the non-fungible token to approve
|
|
* @returns {Boolean} Return whether the approval was successful or not
|
|
*/
|
|
async Approve(ctx, approved, tokenId) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const sender = ctx.clientIdentity.getID();
|
|
|
|
const nft = await this._readNFT(ctx, tokenId);
|
|
|
|
// Check if the sender is the current owner of the non-fungible token
|
|
// or an authorized operator of the current owner
|
|
const owner = nft.owner;
|
|
const operatorApproval = await this.IsApprovedForAll(ctx, owner, sender);
|
|
if (owner !== sender && !operatorApproval) {
|
|
throw new Error('The sender is not the current owner nor an authorized operator');
|
|
}
|
|
|
|
// Update the approved client of the non-fungible token
|
|
nft.approved = approved;
|
|
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
|
|
await ctx.stub.putState(nftKey, Buffer.from(JSON.stringify(nft)));
|
|
|
|
// Emit the Approval event
|
|
const tokenIdInt = parseInt(tokenId);
|
|
const approvalEvent = { owner: owner, approved: approved, tokenId: tokenIdInt };
|
|
ctx.stub.setEvent('Approval', Buffer.from(JSON.stringify(approvalEvent)));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* SetApprovalForAll enables or disables approval for a third party ("operator")
|
|
* to manage all of message sender's assets
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} operator A client to add to the set of authorized operators
|
|
* @param {Boolean} approved True if the operator is approved, false to revoke approval
|
|
* @returns {Boolean} Return whether the approval was successful or not
|
|
*/
|
|
async SetApprovalForAll(ctx, operator, approved) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const sender = ctx.clientIdentity.getID();
|
|
|
|
const approval = { owner: sender, operator: operator, approved: approved };
|
|
const approvalKey = ctx.stub.createCompositeKey(approvalPrefix, [sender, operator]);
|
|
await ctx.stub.putState(approvalKey, Buffer.from(JSON.stringify(approval)));
|
|
|
|
// Emit the ApprovalForAll event
|
|
const approvalForAllEvent = { owner: sender, operator: operator, approved: approved };
|
|
ctx.stub.setEvent('ApprovalForAll', Buffer.from(JSON.stringify(approvalForAllEvent)));
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* GetApproved returns the approved client for a single non-fungible token
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} tokenId the non-fungible token to find the approved client for
|
|
* @returns {Object} Return the approved client for this non-fungible token, or null if there is none
|
|
*/
|
|
async GetApproved(ctx, tokenId) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const nft = await this._readNFT(ctx, tokenId);
|
|
return nft.approved;
|
|
}
|
|
|
|
/**
|
|
* IsApprovedForAll returns if a client is an authorized operator for another client
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} owner The client that owns the non-fungible tokens
|
|
* @param {String} operator The client that acts on behalf of the owner
|
|
* @returns {Boolean} Return true if the operator is an approved operator for the owner, false otherwise
|
|
*/
|
|
async IsApprovedForAll(ctx, owner, operator) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const approvalKey = ctx.stub.createCompositeKey(approvalPrefix, [owner, operator]);
|
|
const approvalBytes = await ctx.stub.getState(approvalKey);
|
|
let approved;
|
|
if (approvalBytes && approvalBytes.length > 0) {
|
|
const approval = JSON.parse(approvalBytes.toString());
|
|
approved = approval.approved;
|
|
} else {
|
|
approved = false;
|
|
}
|
|
|
|
return approved;
|
|
}
|
|
|
|
// ============== ERC721 metadata extension ===============
|
|
|
|
/**
|
|
* Name returns a descriptive name for a collection of non-fungible tokens in this contract
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @returns {String} Returns the name of the token
|
|
*/
|
|
async Name(ctx) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const nameAsBytes = await ctx.stub.getState(nameKey);
|
|
return nameAsBytes.toString();
|
|
}
|
|
|
|
/**
|
|
* Symbol returns an abbreviated name for non-fungible tokens in this contract.
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @returns {String} Returns the symbol of the token
|
|
*/
|
|
async Symbol(ctx) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const symbolAsBytes = await ctx.stub.getState(symbolKey);
|
|
return symbolAsBytes.toString();
|
|
}
|
|
|
|
/**
|
|
* TokenURI returns a distinct Uniform Resource Identifier (URI) for a given token.
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {string} tokenId The identifier for a non-fungible token
|
|
* @returns {String} Returns the URI of the token
|
|
*/
|
|
async TokenURI(ctx, tokenId) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const nft = await this._readNFT(ctx, tokenId);
|
|
return nft.tokenURI;
|
|
}
|
|
|
|
// ============== ERC721 enumeration extension ===============
|
|
|
|
/**
|
|
* TotalSupply counts non-fungible tokens tracked by this contract.
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @returns {Number} Returns a count of valid non-fungible tokens tracked by this contract,
|
|
* where each one of them has an assigned and queryable owner.
|
|
*/
|
|
async TotalSupply(ctx) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
// There is a key record for every non-fungible token in the format of nftPrefix.tokenId.
|
|
// TotalSupply() queries for and counts all records matching nftPrefix.*
|
|
const iterator = await ctx.stub.getStateByPartialCompositeKey(nftPrefix, []);
|
|
|
|
// Count the number of returned composite keys
|
|
let totalSupply = 0;
|
|
let result = await iterator.next();
|
|
while (!result.done) {
|
|
totalSupply++;
|
|
result = await iterator.next();
|
|
}
|
|
return totalSupply;
|
|
}
|
|
|
|
// ============== Extended Functions for this sample ===============
|
|
|
|
/**
|
|
* Set optional information for a token.
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} name The name of the token
|
|
* @param {String} symbol The symbol of the token
|
|
*/
|
|
async Initialize(ctx, name, symbol) {
|
|
|
|
// Check minter authorization - this sample assumes Org1 is the issuer with privilege to initialize contract (set the name and symbol)
|
|
const clientMSPID = ctx.clientIdentity.getMSPID();
|
|
if (clientMSPID !== 'Org1MSP') {
|
|
throw new Error('client is not authorized to set the name and symbol of the token');
|
|
}
|
|
|
|
//check contract options are not already set, client is not authorized to change them once intitialized
|
|
const nameBytes = await ctx.stub.getState(nameKey);
|
|
if (nameBytes !== undefined) {
|
|
throw new Error('contract options are already set, client is not authorized to change them');
|
|
}
|
|
|
|
await ctx.stub.putState(nameKey, Buffer.from(name));
|
|
await ctx.stub.putState(symbolKey, Buffer.from(symbol));
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Mint a new non-fungible token
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} tokenId Unique ID of the non-fungible token to be minted
|
|
* @param {String} tokenURI URI containing metadata of the minted non-fungible token
|
|
* @returns {Object} Return the non-fungible token object
|
|
*/
|
|
async MintWithTokenURI(ctx, tokenId, tokenURI) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
// Check minter authorization - this sample assumes Org1 is the issuer with privilege to mint a new token
|
|
const clientMSPID = ctx.clientIdentity.getMSPID();
|
|
if (clientMSPID !== 'Org1MSP') {
|
|
throw new Error('client is not authorized to mint new tokens');
|
|
}
|
|
|
|
// Get ID of submitting client identity
|
|
const minter = ctx.clientIdentity.getID();
|
|
|
|
// Check if the token to be minted does not exist
|
|
const exists = await this._nftExists(ctx, tokenId);
|
|
if (exists) {
|
|
throw new Error(`The token ${tokenId} is already minted.`);
|
|
}
|
|
|
|
// Add a non-fungible token
|
|
const tokenIdInt = parseInt(tokenId);
|
|
if (isNaN(tokenIdInt)) {
|
|
throw new Error(`The tokenId ${tokenId} is invalid. tokenId must be an integer`);
|
|
}
|
|
const nft = {
|
|
tokenId: tokenIdInt,
|
|
owner: minter,
|
|
tokenURI: tokenURI
|
|
};
|
|
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
|
|
await ctx.stub.putState(nftKey, Buffer.from(JSON.stringify(nft)));
|
|
|
|
// A composite key would be balancePrefix.owner.tokenId, which enables partial
|
|
// composite key query to find and count all records matching balance.owner.*
|
|
// An empty value would represent a delete, so we simply insert the null character.
|
|
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter, tokenId]);
|
|
await ctx.stub.putState(balanceKey, Buffer.from('\u0000'));
|
|
|
|
// Emit the Transfer event
|
|
const transferEvent = { from: '0x0', to: minter, tokenId: tokenIdInt };
|
|
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
|
|
|
|
return nft;
|
|
}
|
|
|
|
/**
|
|
* Burn a non-fungible token
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @param {String} tokenId Unique ID of a non-fungible token
|
|
* @returns {Boolean} Return whether the burn was successful or not
|
|
*/
|
|
async Burn(ctx, tokenId) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
const owner = ctx.clientIdentity.getID();
|
|
|
|
// Check if a caller is the owner of the non-fungible token
|
|
const nft = await this._readNFT(ctx, tokenId);
|
|
if (nft.owner !== owner) {
|
|
throw new Error(`Non-fungible token ${tokenId} is not owned by ${owner}`);
|
|
}
|
|
|
|
// Delete the token
|
|
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
|
|
await ctx.stub.deleteState(nftKey);
|
|
|
|
// Remove a composite key from the balance of the owner
|
|
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [owner, tokenId]);
|
|
await ctx.stub.deleteState(balanceKey);
|
|
|
|
// Emit the Transfer event
|
|
const tokenIdInt = parseInt(tokenId);
|
|
const transferEvent = { from: owner, to: '0x0', tokenId: tokenIdInt };
|
|
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));
|
|
|
|
return true;
|
|
}
|
|
|
|
async _readNFT(ctx, tokenId) {
|
|
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
|
|
const nftBytes = await ctx.stub.getState(nftKey);
|
|
if (!nftBytes || nftBytes.length === 0) {
|
|
throw new Error(`The tokenId ${tokenId} is invalid. It does not exist`);
|
|
}
|
|
const nft = JSON.parse(nftBytes.toString());
|
|
return nft;
|
|
}
|
|
|
|
async _nftExists(ctx, tokenId) {
|
|
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
|
|
const nftBytes = await ctx.stub.getState(nftKey);
|
|
return nftBytes && nftBytes.length > 0;
|
|
}
|
|
|
|
/**
|
|
* ClientAccountBalance returns the balance of the requesting client's account.
|
|
*
|
|
* @param {Context} ctx the transaction context
|
|
* @returns {Number} Returns the account balance
|
|
*/
|
|
async ClientAccountBalance(ctx) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
// Get ID of submitting client identity
|
|
const clientAccountID = ctx.clientIdentity.getID();
|
|
return this.BalanceOf(ctx, clientAccountID);
|
|
}
|
|
|
|
// ClientAccountID returns the id of the requesting client's account.
|
|
// In this implementation, the client account ID is the clientId itself.
|
|
// Users can use this function to get their own account id, which they can then give to others as the payment address
|
|
async ClientAccountID(ctx) {
|
|
//check contract options are already set first to execute the function
|
|
await this.CheckInitialized(ctx);
|
|
|
|
// Get ID of submitting client identity
|
|
const clientAccountID = ctx.clientIdentity.getID();
|
|
return clientAccountID;
|
|
}
|
|
|
|
//Checks that contract options have been already initialized
|
|
async CheckIntitialized(ctx){
|
|
const nameBytes = await ctx.stub.getState(nameKey);
|
|
if (nameBytes === undefined) {
|
|
throw new Error('contract options need to be set before calling any function, call Initialize() to initialize contract');
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = TokenERC721Contract; |