UNPKG

utvoluptas

Version:
441 lines (398 loc) 13.9 kB
import { AccountBalanceQuery, AccountId, Client, Hbar, NftId, PrivateKey, PublicKey, Status, TokenId, TokenMintTransaction, TokenNftInfo, TokenNftInfoQuery, Transaction, TransactionReceipt, TransferTransaction, } from '@hashgraph/sdk'; import keccak256 from 'keccak256'; import { CONFIRMATION_STATUS, TOKEN_ID } from './config/constants.config'; import { logger } from './config/logger.config'; import { callContractFunc } from './contract.utils'; import { getManagerInfo, ManagerInfo } from './manager'; interface SerialInfo { serial: string; node: string; } interface TransactionSignature { signerPublicKey: PublicKey; signature: Uint8Array; } // interface File { // uri: string; // type: string; // metadata: object; // metadata_uri: object; // } // HIP-412 metadata not currently supported by hedera 100 byte metadata limit interface NFTMetadata { name: string; creator: string; // creatorDID: string; description: string; // image: string; // type: string; // files: File[]; // format: string; // properties: object[]; // localization: object[]; } export class HashgraphNames { operatorId: AccountId; operatorKey: PrivateKey; supplyKey: PrivateKey; client: Client; tokenId: TokenId = TokenId.fromString(TOKEN_ID); constructor(operatorId: string, operatorKey: string, supplyKey: string) { this.operatorId = AccountId.fromString(operatorId); this.operatorKey = PrivateKey.fromString(operatorKey); this.supplyKey = PrivateKey.fromString(supplyKey); this.client = Client.forTestnet().setOperator(this.operatorId, this.operatorKey); } static generateMetadata = (domain: string): NFTMetadata => { const metadata: NFTMetadata = { name: domain, creator: 'piefi labs', // creatorDID: '', description: 'Hashgraph Naming service domain', // image: '[cid or path to NFT\'s image]', // type: 'image/jpeg', // TODO: Change this to whatever file type we end up generating for the NFT images // files: [], // format: 'none', // properties: [], // localization: [], }; return metadata; }; /** * @description Simple wrapper around HTS TokenMintTransaction() * @param metadata: {Buffer} The metadata to include on the newly minted NFT * @returns {Promise<TransactionReceipt>} */ private mintNFT = async ( metadata: NFTMetadata, ): Promise<TransactionReceipt> => { try { const mintTx = new TokenMintTransaction() .setTokenId(this.tokenId) .setMetadata([Buffer.from(JSON.stringify(metadata))]) .freezeWith(this.client); const mintTxSign = await mintTx.sign(this.supplyKey); const mintTxSubmit = await mintTxSign.execute(this.client); const mintRx = await mintTxSubmit.getReceipt(this.client); if (mintRx.status._code !== Status.Success._code) { throw new Error('TokenMintTransaction failed'); } return mintRx; } catch (err) { logger.error(err); throw new Error('Failed to mint NFT'); } }; /** * @description Check if a token is associated with a specific account * @param accountId: {AccountId} The account to check if the domain NFT is associated * @returns {Promise<boolean>} */ private isTokenAssociatedToAccount = async ( accountId: AccountId, ): Promise<boolean> => { try { const balanceCheckTx = await new AccountBalanceQuery() .setAccountId(accountId) .execute(this.client); if (!balanceCheckTx) { throw new Error('AccountBalanceQuery Failed'); } const { tokens } = balanceCheckTx; if (tokens) { const tokenOfInterest = tokens._map.get(this.tokenId.toString()); return tokenOfInterest !== undefined; } return false; } catch (err) { logger.error(err); throw new Error('Failed to determine if token is associated to account'); } }; /** * @description Check if a domain exists in the registry * @param domainHash: {Buffer} The hash of the domain to check * @returns {Promise<boolean>} */ private checkDomainExists = async ( domainHash: Buffer, ): Promise<boolean> => { try { const { serial } = await this.getDomainSerial(domainHash); return Number(serial) !== 0; } catch (err) { logger.error(err); throw new Error('Failed to check if domains exists'); } }; /** * @description Register a domain in the smart contract Registry * @param domainHash: {Buffer} The hash of the domain to add to the Registry * @param serial: {number} The serial of the NFT to register * @returns {Promise<number>} */ private registerDomain = async ( domainHash: Buffer, serial: number, ): Promise<number> => { try { // Get manager contract from env const managerInfo = getManagerInfo(); // Add if not present await callContractFunc( managerInfo.contract.id, managerInfo.abi, 'addRecord', [`0x${domainHash.toString('hex')}`, `${serial}`], this.client, ); return CONFIRMATION_STATUS; } catch (err) { logger.error(err); throw new Error('Failed to register Domain'); } }; /** * @description Mints a new domain NFT and records it in the registry * @throws {@link InternalServerError} * @param domain {string} The domain to mint * @param ownerId {string} The owner of the domain to mint * @returns {Promise<number>} */ mintDomain = async ( domain: string, ownerId: string, ): Promise<number> => { let NFTSerial; let domainHash; const accountId = AccountId.fromString(ownerId); try { domainHash = HashgraphNames.generateNFTHash(domain); const domainExists = await this.checkDomainExists(domainHash); if (domainExists) throw new Error('Domain already exists in the registry'); const isAssociated = await this.isTokenAssociatedToAccount(accountId); if (!isAssociated) throw new Error('Wallet must first be associated before a token can be minted'); // Mint the NFT const metadata = HashgraphNames.generateMetadata(domain); const mintRx = await this.mintNFT(metadata); NFTSerial = Number(mintRx.serials[0]); // Register the domain in the Registry await this.registerDomain(domainHash, NFTSerial); return CONFIRMATION_STATUS; } catch (err) { logger.error(err); throw new Error('Failed to mint domain.'); } }; /** * @description Helper function to convert an Uint8Array into an Hedera Transaction type * @param transactionBytes: {Uint8Array} The transaction bytes to be converted */ private static bytesToTransaction = (transactionBytes: Uint8Array): Transaction => { const uint8Array = new Uint8Array(transactionBytes); const transaction: Transaction = Transaction.fromBytes(uint8Array); return transaction; }; /** * @description Executes an HTS TransferTransaction * @param ownerSignature: {TransactionSignature} The signature information for the NFT owner * @param receiverSignature: {TransactionSignature} The signature information for the NFT receiver * @param transactionBytes: {Uint8Array} The transaction bytes to be executed * @returns {Promise<number>} */ transferDomain = async ( ownerSignature: TransactionSignature, receiverSignature: TransactionSignature, transactionBytes: Uint8Array, ): Promise<number> => { try { const transaction: Transaction = HashgraphNames.bytesToTransaction(transactionBytes); transaction .addSignature(ownerSignature.signerPublicKey, ownerSignature.signature) .addSignature(receiverSignature.signerPublicKey, receiverSignature.signature); const submitTransaction = await transaction.execute(this.client); const receipt = await submitTransaction.getReceipt(this.client); if (receipt.status._code !== Status.Success._code) { throw new Error('TransferTransaction failed'); } } catch (err) { throw new Error('Transfer Domain failed'); } return CONFIRMATION_STATUS; }; /** * @description Signs a Hedera transaction * @param signerKey: {string} The private key with which to sign the transaction * @param transactionBytes: {Uint8Array} The bytes for the transaction to be signed * @returns {Promise<Uint8Array>} */ static transferTransactionSign = (signerKey: string, transactionBytes: Uint8Array): TransactionSignature => { const transaction: Transaction = HashgraphNames.bytesToTransaction(transactionBytes); const signerPVKey = PrivateKey.fromString(signerKey); const signature = signerPVKey.signTransaction(transaction); return { signerPublicKey: signerPVKey.publicKey, signature }; }; /** * @description Creates a HTS TransferTransaction and returns it as an Uint8Array * @param domain: {string} The domain for the NFT to transfer * @param NFTOwner: {string} The account id of the NFT owner * @param NFTReceiver: {string} The account id of the NFT receiver * @param purchasePrice: {number} The amount in tinyBar for which the NFT is being purchased * @returns {Uint8Array} */ transferTransactionCreate = async ( domain: string, NFTOwner: string, NFTReceiver: string, purchasePrice: number, ): Promise<Uint8Array> => { try { const fromIdNFT = AccountId.fromString(NFTOwner); const toIdNFT = AccountId.fromString(NFTReceiver); const { serial } = await this.getNFTSerialString(domain); const nodeId = [new AccountId(3)]; const tokenTransferTx = new TransferTransaction() .addNftTransfer(this.tokenId, serial, fromIdNFT, toIdNFT) .addHbarTransfer(toIdNFT, Hbar.fromTinybars(-1 * purchasePrice)) .addHbarTransfer(fromIdNFT, Hbar.fromTinybars(purchasePrice)) .setNodeAccountIds(nodeId) .freezeWith(this.client); return tokenTransferTx.toBytes(); } catch (err) { throw new Error('MultiSig transaction create failed'); } }; /** * @description Generate a hash of the provided domain string * @param domain: {string} The domain string to hash * @returns {Buffer} */ static generateNFTHash = (domain: string): Buffer => { const subDomains = domain.split('.').reverse(); return subDomains.reduce( (prev, curr) => keccak256(prev + curr), Buffer.from([0]), ); }; /** * @description Simple wrapper around callContractFunc for the getSerial smart contract function * @param domainHash: {Buffer} The hash of the domain to query * @param begin: {number} The begin index in the array of nodes of the manager * @param end: {number} The end index in the array of nodes of the manager * @returns {Promise<SerialInfo>} */ private callGetSerial = async ( domainHash: Buffer, begin: number, end: number, ): Promise<SerialInfo> => { try { const managerInfo: ManagerInfo = getManagerInfo(); const result = await callContractFunc( managerInfo.contract.id, managerInfo.abi, 'getSerial', [`0x${domainHash.toString('hex')}`, `${begin}`, `${end}`], this.client, ); return { serial: result[0], node: result[1] }; } catch (err) { logger.error(err); throw new Error('Failed to get owner'); } }; /** * @description Query the registry for the owner of a domain * @param domainHash: {Buffer} The hash of the domain to query * @returns {Promise<SerialInfo>} */ private getDomainSerial = async (domainHash: Buffer): Promise<SerialInfo> => { let decodedResult: SerialInfo = { serial: '0', node: '0' }; try { const managerInfo = getManagerInfo(); const numNodes: number = ( await callContractFunc( managerInfo.contract.id, managerInfo.abi, 'getNumNodes', [], this.client, ) )[0]; const chunkSize = 100; let begin = 0; let end = 0; for (let i = 0; end < numNodes; i += 1) { end = Number((i + 1) * chunkSize); // eslint-disable-next-line no-await-in-loop decodedResult = await this.callGetSerial(domainHash, begin, end); if (Number(decodedResult.serial) !== Number(0)) { // Found the owner break; } begin = end; } return decodedResult; } catch (err) { logger.error(err); throw new Error('Failed to get owner'); } }; /** * @description Simple wrapper around HTS TokenNftInfoQuery() * @param serial: {number} The serial of the NFT to query * @returns {Promise<TokenNftInfo>} */ private getTokenNFTInfo = async ( serial: number, ): Promise<TokenNftInfo> => { try { const nftId = new NftId(this.tokenId, serial); const nftInfo = await new TokenNftInfoQuery() .setNftId(nftId) .execute(this.client); return nftInfo[0]; } catch (err) { logger.error(err); throw new Error('Get NFT info failed'); } }; /** * @description Wrapper around getDomainSerial() that takes a string of the domain * @param domain: {string} The domain to query * @returns {Promise<SerialInfo>} */ getNFTSerialString = async (domain: string): Promise<SerialInfo> => this.getDomainSerial(HashgraphNames.generateNFTHash(domain)); /** * @description Gets the serial for the domain, then queries for the AccountId who owns * that domain. * @param domain: {string} The domain to query * @returns {Promise<AccountId>} */ getWallet = async (domain: string): Promise<AccountId> => { try { const { serial } = await this.getNFTSerialString(domain); const { accountId } = await this.getTokenNFTInfo(Number(serial)); return accountId; } catch (err) { logger.error(err); throw new Error('Failed to get wallet'); } }; }