UNPKG

libnexa-ts

Version:

A pure and powerful Nexa SDK library.

346 lines (309 loc) 11.9 kB
import BufferWriter from "../encoding/bufferwriter"; import Script from "./script/script"; import BN from "../crypto/bn.extension"; import Hash from "../crypto/hash"; import type { GroupIdData } from "../common/interfaces"; import Address from "./address/address"; import BufferUtils from "../utils/buffer.utils"; import ValidationUtils from "../utils/validation.utils"; import CommonUtils from "../utils/common.utils"; import Message from "./message"; import type PrivateKey from "../keys/privatekey"; export enum GroupIdFlag { NONE = 0, COVENANT = 1, // covenants/encumberances -- output script template must match input HOLDS_NEX = 2, // group inputs and outputs must balance NEX, token quantity MUST be 0 GROUP_RESERVED_BITS = 0xFFFF & ~(COVENANT | HOLDS_NEX), DEFAULT = NONE, } /** * Represent Group creation id type in OP_RETURN script */ export enum GroupIdType { /** Legacy token */ LEGACY = 88888888, /** Token */ NRC1 = 88888890, /** NFT Collection */ NRC2 = 88888891, /** NFT that belongs to a collection */ NRC3 = 88888892, } // ts/js does not support enums with bigint, so we use const object for now const AUTH_FLAGS = { /** Is this a controller utxo (forces negative number in amount) */ AUTHORITY: 1n << 63n, /** Can mint tokens */ MINT: 1n << 62n, /** Can melt tokens */ MELT: 1n << 61n, /** Can create authorities */ BATON: 1n << 60n, /** Can change the redeem script */ RESCRIPT: 1n << 59n, /** Can create subgroups */ SUBGROUP: 1n << 58n, NONE: 0n, ALL_FLAG_BITS: 0xffffn << (64n - 16n), } const ACTIVE_FLAG_BITS = AUTH_FLAGS.AUTHORITY | AUTH_FLAGS.MINT | AUTH_FLAGS.MELT | AUTH_FLAGS.BATON | AUTH_FLAGS.RESCRIPT | AUTH_FLAGS.SUBGROUP; /** * A util class with methods for group tokenization. */ export default class GroupToken { public static readonly PARENT_GROUP_ID_SIZE = 32; public static readonly authFlags = { ...AUTH_FLAGS, /** Has all permissions */ ACTIVE_FLAG_BITS, RESERVED_FLAG_BITS: ACTIVE_FLAG_BITS & ~AUTH_FLAGS.ALL_FLAG_BITS }; /** * Calculate a group ID based on the provided inputs. Pass 'null' to opReturnScript if there is not * going to be an OP_RETURN output in the transaction. * * @param outpoint The input outpoint hash hex or buffer * @param opReturnScript opReturn output script * @param authFlag group authority flag (use {@link GroupToken.authFlags}) * @param idFlag group id flag * * @returns Object with group id hash buffer and the nonce bigint */ public static findGroupId(outpoint: string | Uint8Array, opReturnScript: Script | Uint8Array | null, authFlag: bigint, idFlag = GroupIdFlag.DEFAULT): GroupIdData { let hash: Uint8Array; let groupFlags = 0; let nonce = 0n; if (typeof outpoint === 'string') { outpoint = BufferUtils.hexToBuffer(outpoint).reverse(); } if (opReturnScript instanceof Script) { opReturnScript = opReturnScript.toBuffer(); } do { let writer = new BufferWriter(); nonce += 1n; nonce = (nonce & ~this.authFlags.ALL_FLAG_BITS) | authFlag; writer.write(outpoint); if (opReturnScript != null) { writer.writeVarLengthBuf(opReturnScript); } writer.writeUInt64LEBN(BN.fromBigInt(nonce)); hash = Hash.sha256sha256(writer.toBuffer()); groupFlags = (hash[30] << 8) | hash[31]; } while (groupFlags != idFlag); return {hashBuffer: hash, nonce: nonce}; } private static _getGroupAddressBuffer(group: Address | string | Uint8Array): Uint8Array { if (BufferUtils.isBuffer(group)) { return group; } if (CommonUtils.isHexa(group)) { return BufferUtils.hexToBuffer(group as string); } let groupAddress = new Address(group); ValidationUtils.validateArgument(groupAddress.isGroupIdentifierAddress(), 'Invalid group address'); return groupAddress.data; } /** * Translates a group and additional data into a subgroup identifier * * @param group the group/token address or data buffer * @param data the additional data * * @returns the subgroup id buffer */ public static generateSubgroupId(group: Address | string | Uint8Array, data: number | string | Uint8Array): Uint8Array { ValidationUtils.validateArgument(group != null, 'group is missing'); ValidationUtils.validateArgument(data != null, 'data is missing'); group = this._getGroupAddressBuffer(group); if (BufferUtils.isBuffer(data)) { return BufferUtils.concat([group, data]); } if (typeof data === 'number') { let bn = BN.fromNumber(data).toByteArray({ size: 8, endian: "little" }); return BufferUtils.concat([group, bn]); } if (CommonUtils.isHexa(data)) { return BufferUtils.concat([group, BufferUtils.hexToBuffer(data)]); } return BufferUtils.concat([group, BufferUtils.utf8ToBuffer(data)]); } /** * Extract the parent group from the provided subgroup. * * @remarks * If the input is a group but not subgroup, the group itself return. * * @param subgroup the subgroup address or data buffer * @returns the GroupId buffer */ public static getParentGroupId(subgroup: Address | string | Uint8Array): Uint8Array { let buf = this._getGroupAddressBuffer(subgroup); if (buf.length < this.PARENT_GROUP_ID_SIZE) { throw new Error("Invalid subgroup"); } return Uint8Array.from(buf.subarray(0, 32)); } /** * Get group amount buffer from BigInt to include in output script * * @param amount */ public static getAmountBuffer(amount: bigint): Uint8Array { let bw = new BufferWriter(); if (amount < 0n) { let bn = BN.fromBigInt(BigInt.asUintN(64, amount)); bw.writeUInt64LEBN(bn); } else if (amount < 0x10000n) { bw.writeUInt16LE(Number(amount)); } else if (amount < 0x100000000n) { bw.writeUInt32LE(Number(amount)); } else { let bn = BN.fromBigInt(amount); bw.writeUInt64LEBN(bn); } return bw.toBuffer(); } /** * Get group amount value from Buffer * * @param amountBuf the amount buffer * @param unsigned return value as unsigned bigint, default to false */ public static getAmountValue(amountBuf: Uint8Array, unsigned = false): bigint { let amount = BN.fromBuffer(amountBuf, { endian: 'little' }).toBigInt(); return unsigned ? amount : BigInt.asIntN(64, amount); } /** * @param authFlag the utxo group quantity/authority * @returns the nonce */ public static getNonce(authFlag: bigint): bigint { authFlag = BigInt.asUintN(64, authFlag); return authFlag & ~this.authFlags.ALL_FLAG_BITS; } /** * Check if the group id has the flag * * @param groupId the group id address or data buffer * @param groupIdFlag the group id flag * @returns true if this group id has the flag */ public static hasIdFlag(groupId: Address | string | Uint8Array, groupIdFlag: GroupIdFlag): boolean { groupId = this._getGroupAddressBuffer(groupId); return groupId.length >= this.PARENT_GROUP_ID_SIZE ? (((groupId[30] << 8) | groupId[31]) & groupIdFlag) == groupIdFlag : false; } /** * Check if this authority and flag fit to this group creation * * @param groupId the group id address or data buffer * @param authFlag the output group quantity/authority * @param groupIdFlag optional. the group id flag * @returns true if this is group creation data */ public static isGroupCreation(groupId: Address | string | Uint8Array, authFlag: bigint, groupIdFlag = GroupIdFlag.DEFAULT): boolean { groupId = this._getGroupAddressBuffer(groupId); authFlag = BigInt.asUintN(64, authFlag); let hasNonce = this.getNonce(authFlag) != 0n; let isAuth = this.isAuthority(authFlag); let hasFlag = this.hasIdFlag(groupId, groupIdFlag); return isAuth && hasNonce && hasFlag; } /** * Check if this group is is subgroup * * @param groupId the group id address or data buffer * @returns true if this group id is subgroup */ public static isSubgroup(groupId: Address | string | Uint8Array): boolean { groupId = this._getGroupAddressBuffer(groupId); return groupId.length > this.PARENT_GROUP_ID_SIZE; } /** * Check if the group quantity/authority is Authority flag * * @param authFlag the output group quantity/authority * @returns true if this is authority flag */ public static isAuthority(authFlag: bigint): boolean { return (authFlag & this.authFlags.AUTHORITY) == this.authFlags.AUTHORITY; } /** * Check if the group quantity/authority allows minting * * @param authFlag the output group quantity/authority * @returns true if this flag allows minting. */ public static allowsMint(authFlag: bigint): boolean { return (authFlag & (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.MINT)) == (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.MINT); } /** * Check if the group quantity/authority allows melting * * @param authFlag the output group quantity/authority * @returns true if this flag allows melting. */ public static allowsMelt(authFlag: bigint): boolean { return (authFlag & (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.MELT)) == (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.MELT); } /** * Check if the group quantity/authority allows creation of new authorities * * @param authFlag the output group quantity/authority * @returns true if this flag allows creation of authorities. */ public static allowsRenew(authFlag: bigint): boolean { return (authFlag & (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.BATON)) == (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.BATON); } /** * Check if the group quantity/authority allows rescript * * @param authFlag the output group quantity/authority * @returns true if this flag allows rescripting. */ public static allowsRescript(authFlag: bigint): boolean { return (authFlag & (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.RESCRIPT)) == (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.RESCRIPT); } /** * Check if the group quantity/authority allows creation of subgroups * * @param authFlag the output group quantity/authority * @returns true if this flag allows subgroups */ public static allowsSubgroup(authFlag: bigint): boolean { return (authFlag & (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.SUBGROUP)) == (GroupToken.authFlags.AUTHORITY | GroupToken.authFlags.SUBGROUP); } /** * Verify token description document json signature * * @param jsonDoc the json TDD as string * @param address nexa address that signed the doc * @param signature the signature string. optional - if empty, extract from jsonDoc * * @returns true if signature match */ public static verifyJsonDoc(jsonDoc: string, address: Address | string, signature?: string): boolean { ValidationUtils.validateArgument(jsonDoc != "", 'jsonDoc is missing'); ValidationUtils.validateArgument(address != null, 'group is missing'); let json = jsonDoc.substring(jsonDoc.indexOf('{'), jsonDoc.lastIndexOf('}') + 1); if (signature == null || signature === "") { signature = JSON.parse(jsonDoc)[1]; } let msg = new Message(json); return msg.verify(address, signature as string); } /** * Sign token description document json * * @param jsonDoc the json TDD as string * @param privKey private key to sign on the doc * * @returns the signature string */ public static signJsonDoc(jsonDoc: string, privKey: PrivateKey): string { ValidationUtils.validateArgument(jsonDoc != "", 'jsonDoc is missing'); let json = jsonDoc.substring(jsonDoc.indexOf('{'), jsonDoc.lastIndexOf('}') + 1); // trimming let msg = new Message(json); return msg.sign(privKey); } }