It will take some time to fetch & install all packages and dependencies, and when itâs all done you will have something like the below.
And when you run it you will see the template application interface.
Then we will add the dependencies for some react components from material-ui and necessary libraries from concordium-web-sdk and concordium-web-wallet-helper. To do that run the command below and yarn will install all specified packages.
Open up the App.tsx file and remove everything inside the App(). First, add the helpers from the concordium-browser-wallet-api-helpers and then add the connect() function. When you do that, you will be able to connect your wallet, it looks terrible but we will work on that.
Now, first we need to clarify something that you may already know. But in order to interact with a smart contract deployed on-chain you have to use SDKs and the best way is to implement a client using it. Since our project is a react application, Concordium web-SDK is the one we need. Almost all of the code is reusable for any project (might need some changes depending on SDK updates)
Create another folder called âmodulesâ and add a file for the clientâs helper functions implementation(CIS2ContractHelper.ts).
import { WalletApi } from "@concordium/browser-wallet-api-helpers";
import { Buffer } from "buffer/";
import {
ContractAddress,
AccountTransactionType,
UpdateContractPayload,
serializeUpdateContractParameters,
ModuleReference,
InitContractPayload,
TransactionStatusEnum,
TransactionSummary,
CcdAmount,
} from "@concordium/web-sdk";
/**
* Waits for the input transaction to Finalize.
* @param provider Wallet Provider.
* @param txnHash Hash of Transaction.
* @returns Transaction outcomes.
*/
export function waitForTransaction(
provider: WalletApi,
txnHash: string
): Promise<Record<string, TransactionSummary> | undefined> {
return new Promise((res, rej) => {
_wait(provider, txnHash, res, rej);
});
}
export function ensureValidOutcome(
outcomes?: Record<string, TransactionSummary>
): Record<string, TransactionSummary> {
if (!outcomes) {
throw Error("Null Outcome");
}
let successTxnSummary = Object.keys(outcomes)
.map((k) => outcomes[k])
.find((s) => s.result.outcome === "success");
if (!successTxnSummary) {
let failures = Object.keys(outcomes)
.map((k) => outcomes[k])
.filter((s) => s.result.outcome === "reject")
.map((s) => (s.result as any).rejectReason.tag)
.join(",");
throw Error(`Transaction failed, reasons: ${failures}`);
}
return outcomes;
}
/**
* Uses Contract Schema to serialize the contract parameters.
* @param contractName Name of the Contract.
* @param schema Buffer of Contract Schema.
* @param methodName Contract method name.
* @param params Contract Method params in JSON.
* @returns Serialize buffer of the input params.
*/
export function serializeParams<T>(
contractName: string,
schema: Buffer,
methodName: string,
params: T
): Buffer {
return serializeUpdateContractParameters(
contractName,
methodName,
params,
schema
);
}
export function _wait(
provider: WalletApi,
txnHash: string,
res: (p: Record<string, TransactionSummary> | undefined) => void,
rej: (reason: any) => void
) {
setTimeout(() => {
provider
.getJsonRpcClient()
.getTransactionStatus(txnHash)
.then((txnStatus) => {
if (!txnStatus) {
return rej("Transaction Status is null");
}
console.info(`txn : ${txnHash}, status: ${txnStatus?.status}`);
if (txnStatus?.status === TransactionStatusEnum.Finalized) {
return res(txnStatus.outcomes);
}
_wait(provider, txnHash, res, rej);
})
.catch((err) => rej(err));
}, 1000);
}
export function parseContractAddress(
outcomes: Record<string, TransactionSummary>
): ContractAddress {
for (const blockHash in outcomes) {
const res = outcomes[blockHash];
if (res.result.outcome === "success") {
for (const event of res.result.events) {
if (event.tag === "ContractInitialized") {
return {
index: toBigInt((event as any).address.index),
subindex: toBigInt((event as any).address.subindex),
};
}
}
}
}
throw Error(`unable to parse Contract Address from input outcomes`);
}
export function toBigInt(num: BigInt | number): bigint {
return BigInt(num.toString(10));
}
const MICRO_CCD_IN_CCD = 1000000;
export function toCcd(ccdAmount: bigint): CcdAmount {
return new CcdAmount(ccdAmount * BigInt(MICRO_CCD_IN_CCD));
}
Now, we are ready to implement our contract interaction functions including the initContract and the updateContract. Create a file for CIS2ContractClient.ts
In the first function, initContract() we invoke our initialize function in the contract using the schema, moduleRef, and the contractName. In order to make the transaction, we need the AccountTransactionType and will use the WalletApi from the browser-wallet-api-helpers. Using the wallet provider we can send a transaction by providing the parameters. Then we will wait for the transaction to be finalized and parse the return output.
The second function is the updateContract(), we use this function for all operations in that we want to make a state change. This could be a transferring of an asset, minting a token, or burning it. It uses the schema, the moduleRef, the contractName, and the methodName (entrypoint) and calls AccountTransactionâs Update enum which specifies the type of the transaction that is going to be signed.
import { WalletApi } from "@concordium/browser-wallet-api-helpers";
import { Buffer } from "buffer/";
import {
waitForTransaction,
ensureValidOutcome,
serializeParams,
_wait,
parseContractAddress,
toBigInt,
toCcd
} from "./CIS2ContractHelpers";
import {
ContractAddress,
AccountTransactionType,
UpdateContractPayload,
serializeUpdateContractParameters,
ModuleReference,
InitContractPayload,
TransactionStatusEnum,
TransactionSummary,
CcdAmount,
} from "@concordium/web-sdk";
export interface Cis2ContractInfo {
schemaBuffer: Buffer;
contractName: string;
moduleRef: ModuleReference;
tokenIdByteSize: number;
}
/**
* Initializes a Smart Contract.
* @param provider Wallet Provider.
* @param moduleRef Contract Module Reference. Hash of the Deployed Contract Module.
* @param schemaBuffer Buffer of Contract Schema.
* @param contractName Name of the Contract.
* @param account Account to Initialize the contract with.
* @param maxContractExecutionEnergy Maximum energy allowed to execute.
* @param ccdAmount CCD Amount to initialize the contract with.
* @returns Contract Address.
*/
export async function initContract<T>(provider: WalletApi,
contractInfo: Cis2ContractInfo,
account: string,
params?: T,
serializedParams?: Buffer,
maxContractExecutionEnergy = BigInt(999),
ccdAmount = BigInt(0)): Promise<ContractAddress> {
const { moduleRef, schemaBuffer, contractName } = contractInfo;
let txnHash = await provider.sendTransaction(
account,
AccountTransactionType.InitContract,
{
amount: toCcd(ccdAmount),
moduleRef,
initName: contractName,
param: serializedParams || Buffer.from([]),
maxContractExecutionEnergy,
} as InitContractPayload,
params || {},
schemaBuffer.toString("base64"),
2 //schema version
);
let outcomes = await waitForTransaction(provider, txnHash);
outcomes = ensureValidOutcome(outcomes);
return parseContractAddress(outcomes);
}
/**
* Updates a Smart Contract.
* @param provider Wallet Provider.
* @param contractName Name of the Contract.
* @param schema Buffer of Contract Schema.
* @param paramJson Parameters to call the Contract Method with.
* @param account Account to Update the contract with.
* @param contractAddress Contract Address.
* @param methodName Contract Method name to Call.
* @param maxContractExecutionEnergy Maximum energy allowed to execute.
* @param amount CCD Amount to update the contract with.
* @returns Update contract Outcomes.
*/
export async function updateContract<T>(
provider: WalletApi,
contractInfo: Cis2ContractInfo,
paramJson: T,
account: string,
contractAddress: { index: number; subindex: number },
methodName: string,
maxContractExecutionEnergy: bigint = BigInt(9999),
amount: bigint = BigInt(0)
): Promise<Record<string, TransactionSummary>> {
const { schemaBuffer, contractName } = contractInfo;
const parameter = serializeParams(
contractName,
schemaBuffer,
methodName,
paramJson
);
let txnHash = await provider.sendTransaction(
account,
AccountTransactionType.Update,
{
maxContractExecutionEnergy,
address: {
index: BigInt(contractAddress.index),
subindex: BigInt(contractAddress.subindex),
},
message: parameter,
amount: toCcd(amount),
receiveName: `${contractName}.${methodName}`,
} as UpdateContractPayload,
paramJson as any,
schemaBuffer.toString("base64"),
2 //Schema Version
);
let outcomes = await waitForTransaction(provider, txnHash);
return ensureValidOutcome(outcomes);
}
Since we have the helpers and the client code ready, can move on to the next section. Which is implementing contract interactions.
Init Component
In this component, we will handle the smart contract instance creation. It will extract and give the verify_key as a parameter to the cis2-multi smart contract. In components folder create a file called Init.tsx which will invoke the init method in the contract using the client we implemented in the previous section. It will return a button for creating a new instance, and get inputs from the user including the account, schema, moduleRef, contractName, and the verify_key.
In the next section we will mint the token but in order to do that we will need a signature because our solution depends on it, by looking at the signature our contract will understand that we are verified and older than 18, right? So we need to communicate with the verifier. Meaning, we will complete all the steps that are required like asking for the statement and challenge from the verifier. When we have these from the server (remember we are gonna send HTTP/2 GET requests to the endpoints) we will use the provider again (WalletApi) to request proof from the user for a given statement using the specific challenge. Then we will POST the proof in order to get verified and receive back the signature.
Letâs divide the logic into two parts and first implement a client that handles all the API requests. Create a file in the modules for our VerifierBackendClient.ts. We will need the IdStatement and IdProofOutput from the web-SDK and add the following including getChallenge(), getStatement(), and getSignature()
import { IdStatement, IdProofOutput } from '@concordium/web-sdk';
/**
* Fetch a challenge from the backend
*/
export async function getChallenge(verifier: string, accountAddress: string) {
const response = await fetch(`${verifier}/challenge?address=` + accountAddress, { method: 'get' });
const body = await response.json();
return body.challenge;
}
/**
* Fetch the statement to prove from the backend
*/
export async function getStatement(verifier: string): Promise<IdStatement> {
const response = await fetch(`${verifier}/statement`, { method: 'get' });
const body = await response.json();
return JSON.parse(body);
}
/**
* Authorize with the backend, and get a auth token.
*/
export async function getSignature(verifier: string, challenge: string, proof: IdProofOutput) {
const response = await fetch(`${verifier}/prove`, {
method: 'post',
headers: new Headers({ 'content-type': 'application/json' }),
body: JSON.stringify({ challenge, proof }),
});
if (!response.ok) {
throw new Error('Unable to authorize');
}
const body = await response.text();
if (body) {
return body;
}
throw new Error('Unable to authorize');
}
Nice, now we can invoke the verifier using these endpoints. Now we should create a button that calls those endpoints to get the statement, challenge, and send back the proof. Create another component for the âGet Signatureâ button and add the code below. It implements the logic mentioned in orderly.
Create a file for the Mint.tsx component, we will use this to invoke our mint() function implemented on the smart contract. In order to do that, of course we will need to use the client we implemented in the previous section that calls Update type. Letâs look at the MintParams struct in the contract. It expects an address, an object/map that holds the tokens in a form of [tokenId as a key, and <TokenMetadata(which is URL & hash), Amount>] as the value. So we have to create this object and send it as a parameter. Then we will just call the client with the provider(Wallet), account, parameters, contractInfo(including schema, moduleRef, contractName), NFT contractâs index, method name (âmintâ), and maxEnergy.
Letâs create a form to collect required information for minting a token. What do we need? A metadata URL, amount of tokens, a signature that verifies the minter is older than 18, and the contract index, right? Add the following to the Mint.tsx, it is a bit long code but simply the form takes all required values from the user and checks whether are they valid or not. If they are it calls the mint() function with the proper parameters.