# Academic certificates with Truffle
In this tutorial, you will:
- Create a DApp that generates an academic certificate.
- Deploy the DApp on a public Ethereum node using Chainstack.
The contract and the Truffle configuration are in the GitHub repository.
# Prerequisites
- Chainstack account to deploy an Ethereum node.
- Truffle Suite to create and deploy contracts.
# Overview
To get from zero to a deployed DApp on the Ethereum mainnet, do the following:
- With Chainstack, create a public chain project.
- With Chainstack, join the Ethereum mainnet.
- With Chainstack, access your Ethereum node credentials.
- With Truffle, create and compile the DApp contract.
- With Truffle, deploy the contract to your local development network.
- With Truffle, interact with the contract on your local development network.
- With Truffle, create and run the contract test.
- With Truffle, deploy the contract to your Ethereum node running with Chainstack.
# Step-by-step
# Create a public chain project
See Create a project.
# Join the Ethereum mainnet
# Get your Ethereum node access and credentials
See View node access and credentials.
# Create and compile the contracts
- On your machine, create a directory for the contract. Initialize Truffle in the directory:
truffle init
This will generate the Truffle boilerplate structure:
.
├── contracts
│ └── Migrations.sol
├── migrations
│ └── 1_initial_migration.js
├── test
└── truffle-config.js
- Go to the
contracts
directory. In the directory, create two files:Ownable.sol
andDocStamp.sol
.
// Ownable.sol
pragma solidity ^0.5.0;
contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() public {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) onlyOwner public {
require(newOwner != address(0));
emit OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}
This is an ownable contract. The contract implementation is the following:
- Only an authority can generate a certificate. On contract deployment, the authority is the account that deploys the contract. The authority is the contract owner.
- The contract owner can transfer their authority.
// DocStamp.sol
pragma solidity ^0.5.0;
import './Ownable.sol';
contract DocStamp is Ownable {
mapping (bytes32 => address) public records;
event CertificateIssued(bytes32 indexed record, uint256 timestamp, bool returnValue);
function issueCertificate(string calldata name, string calldata details) external onlyOwner {
bytes32 certificate = keccak256(abi.encodePacked(name, details));
require(certificate != keccak256(abi.encodePacked("")));
records[certificate] = msg.sender;
emit CertificateIssued(certificate, block.timestamp, true);
}
function owningAuthority() external view returns (address) {
return owner;
}
function verifyCertificate(string calldata name, string calldata details, bytes32 certificate) external view returns (bool) {
bytes32 certificate2 = keccak256(abi.encodePacked(name, details));
// are certificates the same?
if (certificate == certificate2) {
// does the certificate exist on the blockchain?
if (records[certificate] == owner) {
return true;
}
}
return false;
}
}
This is the main contract. The contract handles the generation and verification of certificates.
issueCertificate()
— generates a certificate by calculating a hash of the student name and details.- Can be called only by the owner.
- Emits a certificate generation event with the timestamp.
- The issuer puts the certificate on the blockchain by storing it in the global variable
records
by passingrecords[certificate] = msg.sender
.
owningAuthority()
— returns the address of issuer/authority.verifyCertificate()
— calculates a hash of the student name and details, and checks if the contract is on the blockchain.- Can be called by anyone.
- Create
2_deploy_contracts.js
in themigrations
directory.
var DocStamp = artifacts.require("./DocStamp.sol");
module.exports = function(deployer) {
deployer.deploy(DocStamp);
};
This will create the contracts deployment instructions for Truffle.
TIP
Since DocStamp inherits from Ownable, Ownable will be deployed together with DocStamp.
- Compile the contracts:
truffle compile
This will compile the contracts and put them in your build/contracts
directory in the .json
format.
# Deploy the contract to your local development network
- Start the development network on your machine:
truffle develop
- Without exiting the Truffle console, deploy the contract to the local development network:
truffle(develop)> migrate
This will deploy the contract to the development network as specified in truffle-config.js
.
# Interact with the contract on your local development network
- In your Truffle console, create an instance of the deployed contract:
let instance = await DocStamp.deployed()
You can run instance
to see the contract object ABI, bytecode, and methods.
- Declare the contract owner:
let owner = await instance.owningAuthority()
You can run owner
to see the account that deployed the contract and owns the contract.
- Issue the certificate:
let result = await instance.issueCertificate("John", "graduate", {from: owner})
This issues the certificate.
Run result.logs
to view the full certificate details.
WARNING
Running result
will not print the certificate details in Truffle console. You must run result.logs
.
See also Processing transaction results.
Example output:
logIndex: 0,
transactionIndex: 0,
transactionHash: '0xb3ef241d76bd4d3a3d92ad4fd382785589033a4f561baa2895136a3315b3561b',
blockHash: '0x29343b9fc5b88bb8c85287463a37a00e8fecce36553880365ca5395d9fb18eeb',
blockNumber: 7,
address: '0x3113Aa54D455142a254b43b83FB16c18eD30ba33',
type: 'mined',
id: 'log_dbbbec7e',
event: 'CertificateIssued',
args: Result {
'0': '0x837e31a66aa8eec0d7adfd41f84175803ddcae69afd451598f2672f652b2c153',
'1': [BN],
'2': true,
__length__: 3,
record: '0x837e31a66aa8eec0d7adfd41f84175803ddcae69afd451598f2672f652b2c153',
timestamp: [BN],
returnValue: true
Note the record
value in the output. This is the hash of the certificate values: name and details. You will need this hash to create the contract test later in this tutorial.
- Run the certificate verification:
let verify = await instance.verifyCertificate("NAME", "DETAILS", "CERTIFICATE_HASH", {from: owner})
where
- NAME — the student name on the certificate.
- DETAILS — any details.
- CERTIFICATE_HASH — the hash of DETAILS and NAME. You should have received this hash in the
record
field at the previous step by runningresult.logs
.
Example:
let verified = await instance.verifyCertificate("John", "graduate", "0x837e31a66aa8eec0d7adfd41f84175803ddcae69afd451598f2672f652b2c153", {from: owner})
Running verify
will now print true
if there is a match, and false
if there is no match.
# Test the contract
- Navigate to the
test
directory. - Create a
test.js
file:
const DocStamp = artifacts.require('./DocStamp.sol')
contract('DocStamp', function(accounts) {
it('should issue a certificate', async function() {
const account = accounts[0]
try {
const instance = await DocStamp.deployed()
await instance.issueCertificate("NAME", "DETAILS")
const authority = await instance.owningAuthority()
assert.equal(authority, account)
} catch(error) {
assert.equal(error, undefined)
}
})
it('should verify a certificate', async function() {
const account = accounts[0]
try {
const instance = await DocStamp.deployed()
const verified = await instance.verifyCertificate("NAME", "DETAILS", "CERTIFICATE_HASH")
assert.equal(verified, true)
} catch(error) {
assert.equal(error, undefined)
}
})
})
where
- NAME — the student name on the certificate.
- DETAILS — any details.
- CERTIFICATE_HASH — the hash of DETAILS and NAME. You should have received this hash in the
record
field at the previous step by runningresult.logs
.
Example:
const DocStamp = artifacts.require('./DocStamp.sol')
contract('DocStamp', function(accounts) {
it('should issue a certificate', async function() {
const account = accounts[0]
try {
const instance = await DocStamp.deployed()
await instance.issueCertificate("John", "graduate")
const authority = await instance.owningAuthority()
assert.equal(authority, account)
} catch(error) {
assert.equal(error, undefined)
}
})
it('should verify a certificate', async function() {
const account = accounts[0]
try {
const instance = await DocStamp.deployed()
const verified = await instance.verifyCertificate("John", "graduate", "0x837e31a66aa8eec0d7adfd41f84175803ddcae69afd451598f2672f652b2c153")
assert.equal(verified, true)
} catch(error) {
assert.equal(error, undefined)
}
})
})
- Run the test:
truffle test
The test run output should be Passing
.
See also
# Deploy the contract to your Ethereum node
- Install
HDWalletProvider
.
HDWalletProvider is Truffle's separate npm package used to sign transactions.
Run:
npm install @truffle/hdwallet-provider
- Edit
truffle-config.js
to add:
HDWalletProvider
- Your Ethereum node access and credentials
const HDWalletProvider = require("@truffle/hdwallet-provider");
const mnemonic = 'misery walnut expose ...';
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 9545,
network_id: "5777"
},
mainnet: {
provider: () => new HDWalletProvider(mnemonic, "RPC_ENDPOINT"),
network_id: 1,
gas: 4500000,
gasPrice: 10000000000
}
}
};
where
mainnet
— any network name that you will pass to thetruffle migrate --network
command.HDWalletProvider
— Truffle's custom provider to sign transactions.mnemonic
— your mnemonic that generates your accounts. You can also generate a mnemonic online with Mnemonic Code Converter. Make sure you generate a 15 word mnemonic.- RPC_ENDPOINT — your Ethereum node RPC endpoint with username and password. The format is
https://user-name:pass-word-pass-word-pass-word@nd-123-456-789.p2pify.com
. See also View node access and credentials. network_id
— the Ethereum mainnet network ID:1
.
Example:
const HDWalletProvider = require("@truffle/hdwallet-provider");
const mnemonic = "misery walnut expose ...";
module.exports = {
networks: {
development: {
host: "127.0.0.1",
port: 9545,
network_id: "5777"
},
mainnet: {
provider: () => new HDWalletProvider(mnemonic, "https://user-name:pass-word-pass-word-pass-word@nd-123-456-789.p2pify.com"),
network_id: 1,
gas: 4500000,
gasPrice: 10000000000
}
}
};
- Run:
truffle migrate --network mainnet
This will engage 2_deploy_contracts.js
and deploy the contract to the Ethereum mainnet as specified in truffle-config.js
.
WARNING
You have to use the mainnet ether to deploy the contract to the Ethereum mainnet.