libnexa-ts
Version:
A pure and powerful Nexa SDK library.
298 lines (266 loc) • 12.9 kB
text/typescript
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());
}
}