UNPKG

libnexa-ts

Version:

A pure and powerful Nexa SDK library.

298 lines (266 loc) 12.9 kB
import PublicKey from "../../keys/publickey"; import ValidationUtils from "../../utils/validation.utils"; import Address from "../address/address"; import Script from "./script"; import ScriptOpcode, { Opcode } from "./opcode"; import CommonUtils from "../../utils/common.utils"; import BN from "../../crypto/bn.extension"; import BufferReader from "../../encoding/bufferreader"; import BufferUtils from "../../utils/buffer.utils"; import Signature from "../../crypto/signature"; import { AddressType } from "../address/address-formatter"; import GroupToken, { GroupIdType } from "../grouptoken"; /** * A factory class for creating scripts with predefined configurations and flags. */ export default class ScriptFactory { /** * @param to destination address or public key * @param groupId group id buffer or group address or hex id - only if its token output script * @param groupAmount optional. quantity amount buffer or bigint - only if its token output script * * @returns a new pay to public key / script template output for the given address or public key */ public static buildScriptTemplateOut(to: string | Address | PublicKey, groupId?: string | Address | Uint8Array, groupAmount?: Uint8Array | bigint): Script { ValidationUtils.validateArgument(to != null, "to", "must provide an argument"); ValidationUtils.validateArgument(to instanceof PublicKey || to instanceof Address || typeof to === 'string', "to", "must be address or pubkey"); ValidationUtils.validateArgument((groupId == null && groupAmount == null) || (groupId != null && groupAmount != null), "group data", "both must present or both not present"); to = this.parseAddress(to); ValidationUtils.validateArgument(to.isPayToScriptTemplate(), 'Invalid destination address (not a script template)'); if (typeof groupId === 'string') { if (CommonUtils.isHexa(groupId)) { groupId = BufferUtils.hexToBuffer(groupId); } else { let groupIdAddr = Address.fromString(groupId); ValidationUtils.validateArgument(groupIdAddr.isGroupIdentifierAddress(), 'Invalid group id address (not a group)'); groupId = groupIdAddr.data; } } else if (groupId instanceof Address) { ValidationUtils.validateArgument(groupId.isGroupIdentifierAddress(), 'Invalid group id address (not a group)'); groupId = groupId.data; } if (typeof groupAmount === 'bigint') { groupAmount = GroupToken.getAmountBuffer(groupAmount); } let bfr = new BufferReader(to.data).readVarLengthBuffer(); let s = Script.fromBuffer(bfr); if (groupId != null && groupAmount != null) { ValidationUtils.validateArgument(BufferUtils.isBuffer(groupId), 'groupId'); ValidationUtils.validateArgument(BufferUtils.isBuffer(groupAmount), 'groupAmount'); // replace OP_0 with group data s.chunks = s.chunks.slice(1); s.prepend(groupAmount).prepend(groupId); } return s; } /** * @param data the data to embed in the output * @param encoding the type of encoding of the string * * @returns a new OP_RETURN script with data */ public static buildDataOut(data?: string | Uint8Array | Script, encoding?: 'hex' | 'utf8'): Script { ValidationUtils.validateArgument(!data || typeof data === 'string' || BufferUtils.isBuffer(data) || data instanceof Script, "data"); if (typeof data === 'string') { data = encoding === 'hex' ? BufferUtils.hexToBuffer(data) : BufferUtils.utf8ToBuffer(data); } let s = Script.empty().add(Opcode.OP_RETURN); if (data != null) { s.add(data); } return s; } /** * @param address the pay to address * @param groupId optional. only for p2st addresses * @param groupAmount optional. only for p2st addresses * * @return an output script built from the address */ public static buildOutFromAddress(address: Address | string, groupId?: string | Address | Uint8Array, groupAmount?: Uint8Array | bigint): Script { if (typeof address === 'string') { address = Address.fromString(address); } if (address.isPayToPublicKeyHash()) { return this.buildPublicKeyHashOut(address); } else if (address.isPayToScriptTemplate()) { return this.buildScriptTemplateOut(address, groupId, groupAmount); } throw new Error(`Invalid address type: ${address.type}`); } /** * Builds a scriptSig (a script for an input) that signs a script template * output script. * * @param template the template script or or undefined for well-known * @param constraint the constraint script or undefined for no constraint script * @param satisfier the satisfier script or buffer or undefined for no satisfier */ public static buildScriptTemplateIn(template?: Script, constraint?: Script, satisfier?: Uint8Array | Script): Script { ValidationUtils.validateArgument(template instanceof Script || !template, "template"); ValidationUtils.validateArgument(constraint instanceof Script || !constraint, "constraint"); ValidationUtils.validateArgument(satisfier instanceof Script || BufferUtils.isBuffer(satisfier) || !satisfier, "satisfier"); let script = Script.empty() if (template instanceof Script) { script.add(template.toBuffer()); } if (constraint instanceof Script) { script.add(constraint.toBuffer()); } if (BufferUtils.isBuffer(satisfier)) { satisfier = Script.fromBuffer(satisfier); } if (satisfier instanceof Script) { script.add(satisfier); } return script; } private static parseAddress(address: string | Address | PublicKey, type?: AddressType): Address { if (typeof address === 'string') { return Address.fromString(address); } else if (address instanceof PublicKey) { return Address.fromPublicKey(address, address.network, type); } return address; } /** * @param to - destination address or public key * * @returns a new pay to public key hash output for the given * address or public key */ public static buildPublicKeyHashOut(to: string | Address | PublicKey): Script { ValidationUtils.validateArgument(to != null, "to", "must provide an argument"); ValidationUtils.validateArgument(to instanceof PublicKey || to instanceof Address || typeof to === 'string', "to", "must be address or pubkey"); to = this.parseAddress(to, AddressType.PayToPublicKeyHash); return Script.empty().add(Opcode.OP_DUP).add(Opcode.OP_HASH160).add(to.data).add(Opcode.OP_EQUALVERIFY).add(Opcode.OP_CHECKSIG); } /** * Builds a scriptSig (a script for an input) that signs a public key hash * output script. (SIGHASH_ALL only) * * @param publicKey * @param signature a Signature object, or the signature in DER canonical encoding */ public static buildPublicKeyHashIn(publicKey: PublicKey, signature: Signature | Uint8Array): Script { ValidationUtils.validateArgument(publicKey instanceof PublicKey, "publicKey"); ValidationUtils.validateArgument(signature instanceof Signature || BufferUtils.isBuffer(signature), "signature"); if (signature instanceof Signature) { signature = signature.toBuffer(); } let script = Script.empty() .add(signature) .add(publicKey.toBuffer()); return script; } /** * Build OP_RETURN output script for Legacy Token Description * * @param ticker the ticker as utf8. * @param name the ticker as utf8. * @param docUrl optional. the description document url * @param docHash optional. the document hash hex. * @param decimals optional. the decimals for the token amount. * * @throws Error if docUrl provided and is invalid * * @returns the output OP_RETURN script */ public static buildTokenDescriptionLegacy(ticker: string, name: string, docUrl?: string, docHash?: string, decimals?: number): Script { ValidationUtils.validateArgument(typeof ticker === 'string' && ticker.length >= 1 && ticker.length <= 8, 'Ticker must be between 1-8 chars'); ValidationUtils.validateArgument(typeof name === 'string' && name.trim() !== '', 'Name is missing'); let s = Script.empty() .add(Opcode.OP_RETURN) .add(BN.fromNumber(GroupIdType.LEGACY).toScriptNumBuffer()) .add(BufferUtils.utf8ToBuffer(ticker)) .add(BufferUtils.utf8ToBuffer(name)); if (docUrl && docUrl.length > 0) { new URL(docUrl); // exception thrown if not valide ValidationUtils.validateArgument(typeof docHash === 'string' && docHash.trim() !== '', 'You must include document hash if you set document url'); s.add(BufferUtils.utf8ToBuffer(docUrl)).add(BufferUtils.hexToBuffer(docHash as string).reverse()); } else { s.add(Opcode.OP_FALSE).add(Opcode.OP_FALSE); } if (typeof decimals === 'number') { ValidationUtils.validateArgument(decimals >= 0 && decimals <= 18, 'decimals must be between 0 and 18'); s.add(decimals <= 16 ? ScriptOpcode.smallInt(decimals) : BN.fromNumber(decimals).toScriptNumBuffer()); } return s; } /** * Build OP_RETURN output script for NRC1 Token Description * * @param ticker the ticker as utf8. * @param name the ticker as utf8. * @param zipURL the zip file url. * @param zipHash the zip file hash hex. * @param decimals the decimals for the token amount. * * @throws Error if zipURL invalid * * @returns the output OP_RETURN script */ public static buildTokenDescription(ticker: string, name: string, zipURL: string, zipHash: string, decimals: number): Script { ValidationUtils.validateArgument(typeof ticker === 'string' && ticker.length >= 2 && ticker.length <= 8, 'Ticker must be 2-8 chars'); ValidationUtils.validateArgument(typeof name === 'string' && name.length >= 2 && name.length <= 25, 'Name must be 2-25 chars'); ValidationUtils.validateArgument(typeof zipURL === 'string' && zipURL.trim() !== '', 'Zip URL is missing'); ValidationUtils.validateArgument(typeof zipHash === 'string' && zipHash.trim() !== '', 'Zip hash is missing'); ValidationUtils.validateArgument(typeof decimals === 'number' && decimals >= 0 && decimals <= 18, 'Decimals must be a number 0-18'); new URL(zipURL); // exception thrown if not valid return Script.empty() .add(Opcode.OP_RETURN) .add(BN.fromNumber(GroupIdType.NRC1).toScriptNumBuffer()) .add(BufferUtils.utf8ToBuffer(ticker)) .add(BufferUtils.utf8ToBuffer(name)) .add(BufferUtils.utf8ToBuffer(zipURL)) .add(BufferUtils.hexToBuffer(zipHash).reverse()) .add(decimals <= 16 ? ScriptOpcode.smallInt(decimals) : BN.fromNumber(decimals).toScriptNumBuffer()); } /** * Build OP_RETURN output script for an NFT Collection Description (NRC2) * * @param ticker the ticker as utf8. * @param name the ticker as utf8. * @param zipURL the zip file url. * @param zipHash the zip file hash hex. * * @throws Error if zipURL invalid * * @returns the output OP_RETURN script */ public static buildNFTCollectionDescription(ticker: string, name: string, zipURL: string, zipHash: string): Script { ValidationUtils.validateArgument(typeof ticker === 'string' && ticker.length >= 2 && ticker.length <= 8, 'Ticker must be 2-8 chars'); ValidationUtils.validateArgument(typeof name === 'string' && name.length >= 2 && name.length <= 25, 'Name must be 2-25 chars'); ValidationUtils.validateArgument(typeof zipURL === 'string' && zipURL.trim() !== '', 'Zip URL is missing'); ValidationUtils.validateArgument(typeof zipHash === 'string' && zipHash.trim() !== '', 'Zip hash is missing'); new URL(zipURL); // exception thrown if not valid return Script.empty() .add(Opcode.OP_RETURN) .add(BN.fromNumber(GroupIdType.NRC2).toScriptNumBuffer()) .add(BufferUtils.utf8ToBuffer(ticker)) .add(BufferUtils.utf8ToBuffer(name)) .add(BufferUtils.utf8ToBuffer(zipURL)) .add(BufferUtils.hexToBuffer(zipHash).reverse()) .add(ScriptOpcode.smallInt(0)); } /** * Build OP_RETURN output script for an NFT that belongs to an NFT Collection (NRC3) * * @param zipURL the zip file url. * @param zipHash the zip file hash hex. * * @throws Error if zipURL invalid * * @returns the output OP_RETURN script */ public static buildNFTDescription(zipURL: string, zipHash: string): Script { ValidationUtils.validateArgument(typeof zipURL === 'string' && zipURL.trim() !== '', 'Zip URL is missing'); ValidationUtils.validateArgument(typeof zipHash === 'string' && zipHash.trim() !== '', 'Zip hash is missing'); new URL(zipURL); // exception thrown if not valid return Script.empty() .add(Opcode.OP_RETURN) .add(BN.fromNumber(GroupIdType.NRC3).toScriptNumBuffer()) .add(BufferUtils.utf8ToBuffer(zipURL)) .add(BufferUtils.hexToBuffer(zipHash).reverse()); } }