mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-17 07:25:10 +00:00
Add ERC721 non-fungible token sample for javascript Chaincode (#406)
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>
This commit is contained in:
parent
b4db1f53a0
commit
b88ec9f4b4
9 changed files with 1401 additions and 0 deletions
458
token-erc-721/README.md
Normal file
458
token-erc-721/README.md
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
# ERC-721 token scenario
|
||||
|
||||
The ERC-721 token smart contract demonstrates how to create and transfer non-fungible tokens.
|
||||
Non-fungible tokens represent ownership over digital or physical assets. Example assets are artworks, houses, tickets, etc.
|
||||
Non-fungible tokens are distinguishable and we can track the ownership of each one separately.
|
||||
|
||||
In ERC-721, there is an account for each participant that holds a balance of tokens.
|
||||
A mint transaction creates a non-fungible token for an owner and adds one token in the owner's account.
|
||||
A transfer transaction changes the ownership of a token from the current owner to a new owner.
|
||||
The transfer also debits one token from the previous owner's account and credits one token to another account.
|
||||
|
||||
In this sample it is assumed that only one organization (played by Org1) is in an issuer role and can mint new tokens into their account, while any organization can transfer tokens from their account to a recipient's account.
|
||||
Accounts could be defined at the organization level or client identity level. In this sample accounts are defined at the client identity level, where every authorized client with an enrollment certificate from their organization implicitly has an account ID that matches their client ID.
|
||||
The client ID is simply a base64-encoded concatenation of the issuer and subject from the client identity's enrollment certificate. The client ID can therefore be considered the account ID that is used as the payment address of a recipient.
|
||||
|
||||
In this tutorial, you will mint and transfer tokens as follows:
|
||||
|
||||
- A member of Org1 uses the `MintWithTokenURI` function to create a new non-fungible token into their account. The `MintWithTokenURI` smart contract function reads the certificate information of the client identity that submitted the transaction using the `GetClientIdentity.GetID()` API and creates a non-fungible token associated with the client ID with the requested token ID.
|
||||
- The same minter client will then use the `TransferFrom` function to transfer a non-fungible token with a requested token ID to the recipient's account. It is assumed that the recipient has provided their account ID to the transfer caller out of band. The recipient can then transfer tokens to other registered users in the same fashion.
|
||||
|
||||
## Bring up the test network
|
||||
|
||||
You can run the ERC-721 token transfer scenario using the Fabric test network. Open a command terminal and navigate to the test network directory in your local clone of the `fabric-samples`. We will operate from the `test-network` directory for the remainder of the tutorial.
|
||||
```
|
||||
cd fabric-samples/test-network
|
||||
```
|
||||
|
||||
Run the following command to start the test network:
|
||||
```
|
||||
./network.sh up createChannel -ca
|
||||
```
|
||||
|
||||
The test network is deployed with two peer organizations. The `createChannel` flag deploys the network with a single channel named `mychannel` with Org1 and Org2 as channel members.
|
||||
The -ca flag is used to deploy the network using certificate authorities. This allows you to use each organization's CA to register and enroll new users for this tutorial.
|
||||
|
||||
## Deploy the smart contract to the channel
|
||||
|
||||
You can use the test network script to deploy the ERC-721 token contract to the channel that was just created. Deploy the smart contract to `mychannel` using the following command:
|
||||
|
||||
```
|
||||
./network.sh deployCC -ccn token_erc721 -ccp ../token-erc-721/chaincode-javascript/ -ccl javascript
|
||||
```
|
||||
|
||||
The above command deploys the chaincode with short name `token_erc721`. The smart contract will use the default endorsement policy of majority of channel members.
|
||||
Since the channel has two members, this implies that we'll need to get peer endorsements from 2 out of the 2 channel members.
|
||||
|
||||
Now you are ready to call the deployed smart contract via peer CLI calls. But let's first create the client identities for our scenario.
|
||||
|
||||
## Register identities
|
||||
|
||||
The smart contract supports accounts owned by individual client identities from organizations that are members of the channel. In our scenario, the minter of the tokens will be a member of Org1, while the recipient will belong to Org2. To highlight the connection between the `GetClientIdentity().GetID()` API and the information within a user's certificate, we will register two new identities using the Org1 and Org2 Certificate Authorities (CA's), and then use the CA's to generate each identity's certificate and private key.
|
||||
|
||||
First, we need to set the following environment variables to use the Fabric CA client (and subsequent commands).
|
||||
```
|
||||
export PATH=${PWD}/../bin:${PWD}:$PATH
|
||||
export FABRIC_CFG_PATH=$PWD/../config/
|
||||
```
|
||||
|
||||
The terminal we have been using will represent Org1. We will use the Org1 CA to create the minter identity. Set the Fabric CA client home to the MSP of the Org1 CA admin (this identity was generated by the test network script):
|
||||
```
|
||||
export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org1.example.com/
|
||||
```
|
||||
|
||||
You can register a new minter client identity using the `fabric-ca-client` tool:
|
||||
```
|
||||
fabric-ca-client register --caname ca-org1 --id.name minter --id.secret minterpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem
|
||||
```
|
||||
|
||||
You can now generate the identity certificates and MSP folder by providing the minter's enroll name and secret to the enroll command:
|
||||
```
|
||||
fabric-ca-client enroll -u https://minter:minterpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem
|
||||
```
|
||||
|
||||
Run the command below to copy the Node OU configuration file into the minter identity MSP folder.
|
||||
```
|
||||
cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp/config.yaml
|
||||
```
|
||||
|
||||
Open a new terminal to represent Org2 and navigate to fabric-samples/test-network. We'll use the Org2 CA to create the Org2 recipient identity. Set the Fabric CA client home to the MSP of the Org2 CA admin:
|
||||
```
|
||||
cd fabric-samples/test-network
|
||||
export PATH=${PWD}/../bin:${PWD}:$PATH
|
||||
export FABRIC_CA_CLIENT_HOME=${PWD}/organizations/peerOrganizations/org2.example.com/
|
||||
```
|
||||
|
||||
You can register a recipient client identity using the `fabric-ca-client` tool:
|
||||
```
|
||||
fabric-ca-client register --caname ca-org2 --id.name recipient --id.secret recipientpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem
|
||||
```
|
||||
|
||||
We can now enroll to generate the recipient's identity certificates and MSP folder:
|
||||
```
|
||||
fabric-ca-client enroll -u https://recipient:recipientpw@localhost:8054 --caname ca-org2 -M ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org2/tls-cert.pem
|
||||
```
|
||||
|
||||
Run the command below to copy the Node OU configuration file into the recipient identity MSP folder.
|
||||
```
|
||||
cp ${PWD}/organizations/peerOrganizations/org2.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp/config.yaml
|
||||
```
|
||||
|
||||
## Mint a non-fungible token
|
||||
|
||||
Now that we have created the identity of the minter, we can invoke the smart contract to mint a non-fungible token.
|
||||
Shift back to the Org1 terminal, we'll set the following environment variables to operate the `peer` CLI as the minter identity from Org1.
|
||||
```
|
||||
export CORE_PEER_TLS_ENABLED=true
|
||||
export CORE_PEER_LOCALMSPID="Org1MSP"
|
||||
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/minter@org1.example.com/msp
|
||||
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
|
||||
export CORE_PEER_ADDRESS=localhost:7051
|
||||
export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"
|
||||
```
|
||||
|
||||
The last environment variable above will be utilized within the CLI invoke commands to set the target peers for endorsement, and the target ordering service endpoint and TLS options.
|
||||
|
||||
We can then invoke the smart contract to mint a non-fungible token with a unique token ID `101`:
|
||||
```
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"MintWithTokenURI","Args":["101", "https://example.com/nft101.json"]}'
|
||||
```
|
||||
|
||||
The mint function validated that the client is a member of the minter organization, and then create a new non-fungible token for the minter. We can check the minter client's account balance by calling the `ClientAccountBalance` function.
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountBalance","Args":[]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the minter client ID and returns:
|
||||
```
|
||||
1
|
||||
```
|
||||
|
||||
We can also check the owner of the issued token by calling the `OwnerOf` function.
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"OwnerOf","Args":["101"]}'
|
||||
```
|
||||
|
||||
The function queries the owner of the non-fungible token associated with the token ID and returns:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com
|
||||
```
|
||||
|
||||
## Transfer a non-fungible token
|
||||
|
||||
The minter intends to transfer a non-fungible token to the Org2 recipient, but first the Org2 recipient needs to provide their own account ID as the payment address.
|
||||
A client can derive their account ID from their own public certificate, but to be sure the account ID is accurate, the contract has a `ClientAccountID` utility function that simply looks at the callers certificate and returns the calling client's ID, which will be used as the account ID.
|
||||
Let's prepare the Org2 terminal by setting the environment variables for the Org2 recipient user.
|
||||
```
|
||||
export FABRIC_CFG_PATH=$PWD/../config/
|
||||
export CORE_PEER_TLS_ENABLED=true
|
||||
export CORE_PEER_LOCALMSPID="Org2MSP"
|
||||
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org2.example.com/users/recipient@org2.example.com/msp
|
||||
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt
|
||||
export CORE_PEER_ADDRESS=localhost:9051
|
||||
```
|
||||
|
||||
Using the Org2 terminal, the Org2 recipient user can retrieve their own account ID:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountID","Args":[]}'
|
||||
```
|
||||
|
||||
The function returns of recipient's client ID.
|
||||
The result shows that the subject and issuer is indeed the recipient user from Org2:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com
|
||||
```
|
||||
|
||||
After the Org2 recipient provides their account ID to the minter, the minter can initiate a transfer from their account to the recipient's account.
|
||||
|
||||
To transfer a non-fungible token, minter also needs to provide it's own account ID.
|
||||
Back in the Org1 terminal, the Org1 minter user can retrieve their own account ID:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountID","Args":[]}'
|
||||
```
|
||||
|
||||
The function returns of minter's client ID.
|
||||
The result shows that the subject and issuer is indeed the recipient user from Org1:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com
|
||||
```
|
||||
|
||||
After that, request the transfer of a non-fungible token `101` to the recipient account:
|
||||
```
|
||||
export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com"
|
||||
export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com"
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"TransferFrom","Args":["'"$MINTER"'", "'"$RECIPIENT"'","101"]}'
|
||||
```
|
||||
|
||||
The `TransferFrom` function validates ownership of the given non-fungible token.
|
||||
It will then change the ownership of the non-fungible token from the current owner to the recipient.
|
||||
It will also debit the caller's account and credit the recipient's account. Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist.
|
||||
|
||||
While still in the Org1 terminal, let's request the minter's account balance again:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountBalance","Args":[]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the minter client ID and returns:
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
And then using the Org2 terminal, let's request the recipient's balance:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountBalance","Args":[]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the recipient client ID and returns:
|
||||
```
|
||||
1
|
||||
```
|
||||
|
||||
While still in the Org2 terminal, let's check the current owner of the token.
|
||||
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"OwnerOf","Args":["101"]}'
|
||||
```
|
||||
|
||||
The function queries the owner of the non-fungible token with the token ID and returns:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com
|
||||
```
|
||||
|
||||
Congratulations, you've transferred a non-fungible token! The Org2 recipient can now transfer the token to other registered users in the same manner.
|
||||
|
||||
## 3rd party transfers (Specific Token)
|
||||
|
||||
This sample has another option, which allows an approved 3rd party operator to transfer a non-fungible token on behalf of the token owner. The owner appoves only one specific token to be transferred by the operator. This scenario demonstrates how to approve the operator and transfer a non-fungible token.
|
||||
|
||||
In this scenario, you will approve the operator and transfer a specific non-fungible token as follows:
|
||||
|
||||
- A minter has already created a non-fungible token according to the scenario above.
|
||||
- The same minter client uses the `Approve` function to give the permission for an operator client to transfer a non-fungible token which has a specific token ID on behalf of the minter. It is assumed that the operator has provided their client ID to the `Approve` caller out of band.
|
||||
- The operator client will then use the `TransferFrom` function to transfer the non-fungible token to the recipient's account on behalf of the minter. It is assumed that the recipient has provided their client ID to the `TransferFrom` caller out of band.
|
||||
|
||||
## Register identity for 3rd party operator
|
||||
|
||||
You have already brought up the network and deployed the smart contract to the channel. We will use the same network and smart contract.
|
||||
|
||||
We will use the Org1 CA to create the operator identity.
|
||||
Back in the Org1 terminal, you can register a new operator client identity using the `fabric-ca-client` tool:
|
||||
```
|
||||
fabric-ca-client register --caname ca-org1 --id.name operator --id.secret operatorpw --id.type client --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem
|
||||
```
|
||||
|
||||
You can now generate the identity certificates and MSP folder by providing the operator's enroll name and secret to the enroll command:
|
||||
```
|
||||
fabric-ca-client enroll -u https://operator:operatorpw@localhost:7054 --caname ca-org1 -M ${PWD}/organizations/peerOrganizations/org1.example.com/users/operator@org1.example.com/msp --tls.certfiles ${PWD}/organizations/fabric-ca/org1/tls-cert.pem
|
||||
```
|
||||
|
||||
Run the command below to copy the Node OU configuration file into the operator identity MSP folder.
|
||||
```
|
||||
cp ${PWD}/organizations/peerOrganizations/org1.example.com/msp/config.yaml ${PWD}/organizations/peerOrganizations/org1.example.com/users/operator@org1.example.com/msp/config.yaml
|
||||
```
|
||||
|
||||
## Approve an operator
|
||||
|
||||
The minter intends to approve a non-fungible token to be transferred by the operator, but first the operator needs to provide their own client ID as the payment address.
|
||||
|
||||
Open a 3rd terminal to represent the operator in Org1 and navigate to fabric-samples/test-network. Set the the environment variables for the Org1 operator user.
|
||||
|
||||
```
|
||||
export PATH=${PWD}/../bin:${PWD}:$PATH
|
||||
export FABRIC_CFG_PATH=$PWD/../config/
|
||||
export CORE_PEER_TLS_ENABLED=true
|
||||
export CORE_PEER_LOCALMSPID="Org1MSP"
|
||||
export CORE_PEER_MSPCONFIGPATH=${PWD}/organizations/peerOrganizations/org1.example.com/users/operator@org1.example.com/msp
|
||||
export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
|
||||
export CORE_PEER_ADDRESS=localhost:7051
|
||||
export TARGET_TLS_OPTIONS="-o localhost:7050 --ordererTLSHostnameOverride orderer.example.com --tls --cafile ${PWD}/organizations/ordererOrganizations/example.com/orderers/orderer.example.com/msp/tlscacerts/tlsca.example.com-cert.pem --peerAddresses localhost:7051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt --peerAddresses localhost:9051 --tlsRootCertFiles ${PWD}/organizations/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt"
|
||||
```
|
||||
|
||||
Now the Org1 operator can retrieve their own client ID:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountID","Args":[]}'
|
||||
```
|
||||
|
||||
The function returns of operator's client ID.
|
||||
The result shows that the subject and issuer is indeed the operator user from Org1:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=operator::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com
|
||||
```
|
||||
|
||||
After the Org1 operator provides their client ID to the minter, the minter can approve an operator.
|
||||
Back in the Org1 minter terminal, issue a new non-fungible token with the token ID `102`.
|
||||
And then request the approval for the operator to transfer the token.
|
||||
|
||||
```
|
||||
# Issue a new token
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"MintWithTokenURI","Args":["102", "https://example.com/nft102.json"]}'
|
||||
|
||||
# The owner approves
|
||||
export OPERATOR="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=operator::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com"
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"Approve","Args":["'"$OPERATOR"'", "102"]}'
|
||||
```
|
||||
|
||||
The approve function specified that the operator client can transfer the non-fungible token with the given token ID on behalf of the minter. We can check the operator client's approval by calling the `GetApproved` function.
|
||||
|
||||
Let's request the operator's approval from the Org1 minter terminal.
|
||||
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"GetApproved","Args":["102"]}'
|
||||
```
|
||||
|
||||
The function queries the approval associated with the operator client ID and returns:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=operator::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com
|
||||
```
|
||||
|
||||
## Transfer a non-fungible token
|
||||
|
||||
The operator intends to transfer a non-fungible token to the Org2 recipient on behalf of the minter. The operator has already got the minter client Id and the recipient client ID.
|
||||
|
||||
Back in the 3rd operator terminal, request the transfer of a non-fungible token `102` to the recipient account:
|
||||
|
||||
```
|
||||
export MINTER="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=minter::/C=US/ST=North Carolina/L=Durham/O=org1.example.com/CN=ca.org1.example.com"
|
||||
export RECIPIENT="x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com"
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "102"]}'
|
||||
```
|
||||
|
||||
The `TransferFrom` function validates that the account associated with the calling client ID has the permission to transfer the given token on behalf of the current owner.
|
||||
It will then change the ownership of the non-fungible token from the current owner to the recipient.
|
||||
It will also debit the previous owner's account and credit the recipient's account.
|
||||
It will also remove the operator's permission for this non-fungible token approved by the minter.
|
||||
Note that the sample contract will automatically create an account with zero balance for the recipient, if one does not yet exist.
|
||||
|
||||
While still in the 3rd operator terminal for the operator, let's request the minter's account balance again:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"BalanceOf","Args":["'"$MINTER"'"]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the minter client ID and returns:
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
And then, let's check the current owner of the token.
|
||||
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"OwnerOf","Args":["102"]}'
|
||||
```
|
||||
|
||||
The function queries the owner of the non-fungible token with the token ID and returns:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com
|
||||
```
|
||||
|
||||
While still in the 3rd operator terminal for the operator, let's request the operator's approval from the minter again.
|
||||
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"GetApproved","Args":["102"]}'
|
||||
```
|
||||
|
||||
The function queries the approval associated with the operator client ID and returns no value.
|
||||
|
||||
And then using the Org2 terminal, let's request the recipient's balance:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountBalance","Args":[]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the recipient client ID and returns:
|
||||
```
|
||||
2
|
||||
```
|
||||
|
||||
Congratulations, you've transferred a non-fungible token! The Org2 recipient can now transfer tokens to other registered users in the same manner.
|
||||
|
||||
|
||||
## 3rd party transfers (All tokens)
|
||||
|
||||
This sample has another option, which allows an approved 3rd party operator to transfer all non-fungible tokens on behalf of the token owner. The owner approves all tokens to be transferred by the operator. This scenario demonstrates how to approve the operator and transfer non-fungible tokens.
|
||||
|
||||
In this scenario, you will approve the operator and transfer a non-fungible token as follows:
|
||||
|
||||
- A minter has already created a non-fungible token according to the scenario above.
|
||||
- The same minter client uses the `SetApprovalForAll` function to give the permission for an operator client to transfer all non-fungible tokens on behalf of the minter. It is assumed that the operator has provided their client ID to the `SetApprovalForAll` caller out of band.
|
||||
- The operator client will then use the `TransferFrom` function to transfer the non-fungible token to the recipient's account on behalf of the minter. It is assumed that the recipient has provided their client ID to the `TransferFrom` caller out of band.
|
||||
|
||||
## Approve an operator
|
||||
|
||||
Request the approval for the operator to transfer the token.
|
||||
we assume that the minter has already got the operator client ID as the payment address.
|
||||
|
||||
Back in the Org1 minter terminal, request the approval for the operator to transfer all tokens on behalf of the original owner.
|
||||
```
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"SetApprovalForAll","Args":["'"$OPERATOR"'", "true"]}'
|
||||
```
|
||||
|
||||
The `SetApprovalForAll` function specified that the operator client can transfer any non-fungible tokens on behalf of the minter. We can check the operator client's approval by calling the `IsApprovedForAll` function.
|
||||
|
||||
Let's request the operator's approval from the Org1 minter terminal.
|
||||
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"IsApprovedForAll","Args":["'"$MINTER"'", "'"$OPERATOR"'"]}'
|
||||
```
|
||||
|
||||
The function queries the approval associated with the operator client ID and returns:
|
||||
```
|
||||
true
|
||||
```
|
||||
|
||||
## Transfer a non-fungible token
|
||||
|
||||
The operator intends to transfer a non-fungible token to the Org2 recipient on behalf of the minter. The operator has already got the minter client Id and the recipient client ID.
|
||||
|
||||
Still in the Org1 minter terminal, issue a new non-fungible token with the token ID `103`.
|
||||
```
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"MintWithTokenURI","Args":["103", "https://example.com/nft103.json"]}'
|
||||
|
||||
```
|
||||
|
||||
Back in the 3rd operator terminal, request the transfer of a non-fungible token `103` to the recipient account:
|
||||
|
||||
```
|
||||
peer chaincode invoke $TARGET_TLS_OPTIONS -C mychannel -n token_erc721 -c '{"function":"TransferFrom","Args":[ "'"$MINTER"'", "'"$RECIPIENT"'", "103"]}'
|
||||
```
|
||||
|
||||
The `TransferFrom` function validates that the account associated with the calling client ID has the permission to transfer tokens on behalf of the current owner.
|
||||
It will then change the ownership of the non-fungible token and update balances like the other options.
|
||||
|
||||
While still in the 3rd operator terminal for the operator, let's request the minter's account balance again:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"BalanceOf","Args":["'"$MINTER"'"]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the minter client ID and returns:
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
And then, let's check the current owner of the token.
|
||||
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"OwnerOf","Args":["103"]}'
|
||||
```
|
||||
|
||||
The function queries the owner of the non-fungible token with the token ID and returns:
|
||||
```
|
||||
x509::/C=US/ST=North Carolina/O=Hyperledger/OU=client/CN=recipient::/C=UK/ST=Hampshire/L=Hursley/O=org2.example.com/CN=ca.org2.example.com
|
||||
```
|
||||
|
||||
And then using the Org2 terminal, let's request the recipient's balance:
|
||||
```
|
||||
peer chaincode query -C mychannel -n token_erc721 -c '{"function":"ClientAccountBalance","Args":[]}'
|
||||
```
|
||||
|
||||
The function queries the balance of the account associated with the recipient client ID and returns:
|
||||
```
|
||||
3
|
||||
```
|
||||
|
||||
Congratulations, you've transferred a non-fungible token! The Org2 recipient can now transfer tokens to other registered users in the same manner.
|
||||
|
||||
## Clean up
|
||||
|
||||
When you are finished, you can bring down the test network. The command will remove all the nodes of the test network, and delete any ledger data that you created:
|
||||
```
|
||||
./network.sh down
|
||||
```
|
||||
16
token-erc-721/chaincode-javascript/.editorconfig
Executable file
16
token-erc-721/chaincode-javascript/.editorconfig
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
5
token-erc-721/chaincode-javascript/.eslintignore
Normal file
5
token-erc-721/chaincode-javascript/.eslintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
coverage
|
||||
39
token-erc-721/chaincode-javascript/.eslintrc.js
Normal file
39
token-erc-721/chaincode-javascript/.eslintrc.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
mocha: true
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 8,
|
||||
sourceType: 'script'
|
||||
},
|
||||
extends: "eslint:recommended",
|
||||
rules: {
|
||||
indent: ['error', 4],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always'],
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'no-console': 'off',
|
||||
curly: 'error',
|
||||
eqeqeq: 'error',
|
||||
'no-throw-literal': 'error',
|
||||
strict: 'error',
|
||||
'no-var': 'error',
|
||||
'dot-notation': 'error',
|
||||
'no-tabs': 'error',
|
||||
'no-trailing-spaces': 'error',
|
||||
'no-use-before-define': 'error',
|
||||
'no-useless-call': 'error',
|
||||
'no-with': 'error',
|
||||
'operator-linebreak': 'error',
|
||||
yoda: 'error',
|
||||
'quote-props': ['error', 'as-needed'],
|
||||
'no-constant-condition': ["error", { "checkLoops": false }]
|
||||
}
|
||||
};
|
||||
78
token-erc-721/chaincode-javascript/.gitignore
vendored
Normal file
78
token-erc-721/chaincode-javascript/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
package-lock.json
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
10
token-erc-721/chaincode-javascript/index.js
Normal file
10
token-erc-721/chaincode-javascript/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const tokenERC721Contract = require('./lib/tokenERC721.js');
|
||||
|
||||
module.exports.tokenERC721Contract = tokenERC721Contract;
|
||||
module.exports.contracts = [tokenERC721Contract];
|
||||
406
token-erc-721/chaincode-javascript/lib/tokenERC721.js
Normal file
406
token-erc-721/chaincode-javascript/lib/tokenERC721.js
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
/*
|
||||
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) {
|
||||
// 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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
// 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 SetOption(ctx, name, symbol) {
|
||||
|
||||
// Check minter authorization - this sample assumes Org1 is the issuer with privilege to 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');
|
||||
}
|
||||
|
||||
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 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) {
|
||||
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) {
|
||||
// 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) {
|
||||
// Get ID of submitting client identity
|
||||
const clientAccountID = ctx.clientIdentity.getID();
|
||||
return clientAccountID;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TokenERC721Contract;
|
||||
51
token-erc-721/chaincode-javascript/package.json
Normal file
51
token-erc-721/chaincode-javascript/package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "token-erc721",
|
||||
"version": "0.0.1",
|
||||
"description": "Token-ERC721 contract implemented in JavaScript",
|
||||
"main": "index.js",
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=5"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"pretest": "npm run lint",
|
||||
"test": "nyc mocha --recursive",
|
||||
"mocha": "mocha --recursive",
|
||||
"start": "fabric-chaincode-node start"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"author": "Hyperledger",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fabric-contract-api": "^2.0.0",
|
||||
"fabric-shim": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^4.19.1",
|
||||
"mocha": "^8.0.1",
|
||||
"nyc": "^14.1.1",
|
||||
"sinon": "^6.0.0",
|
||||
"sinon-chai": "^3.2.0"
|
||||
},
|
||||
"nyc": {
|
||||
"exclude": [
|
||||
"coverage/**",
|
||||
"test/**",
|
||||
"index.js",
|
||||
".eslintrc.js"
|
||||
],
|
||||
"reporter": [
|
||||
"text-summary",
|
||||
"html"
|
||||
],
|
||||
"all": true,
|
||||
"check-coverage": false,
|
||||
"statements": 100,
|
||||
"branches": 100,
|
||||
"functions": 100,
|
||||
"lines": 100
|
||||
}
|
||||
}
|
||||
338
token-erc-721/chaincode-javascript/test/tokenERC721.test.js
Normal file
338
token-erc-721/chaincode-javascript/test/tokenERC721.test.js
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue