libnexa-ts
Version:
A pure and powerful Nexa SDK library.
308 lines (274 loc) • 11.6 kB
text/typescript
import type { Networkish } from "../../common/types";
import ValidationUtils from "../../utils/validation.utils";
import { networks } from "../network/network-manager";
import AddressFormatter, { AddressType } from "./address-formatter";
import type { Network } from "../network/network";
import type { AddressDto } from "../../common/interfaces";
import CommonUtils from "../../utils/common.utils";
import PublicKey from "../../keys/publickey";
import Hash from "../../crypto/hash";
import Script from "../script/script";
import type { ScriptElement } from "../script/script";
import { Opcode } from "../script/opcode";
import BufferUtils from "../../utils/buffer.utils";
import BufferWriter from "../../encoding/bufferwriter";
export default class Address {
public readonly data!: Uint8Array;
public readonly network!: Network;
public readonly type!: AddressType;
/**
* Instantiate an address from an address String or Uint8Array, a public key or script hash Uint8Array,
* or an instance of {@link PublicKey} or {@link Script}.
*
* This is an immutable class, and if the first parameter provided to this constructor is an
* `Address` instance, the same argument will be returned.
*
* An address has two key properties: `network` and `type`. The type is either
* `AddressType.PayToPublicKeyHash` (value is the `'P2PKH'` string)
* or `AddressType.PayToScriptTemplate` (the string `'P2ST'`). The network is an instance of {@link Network} or network name.
* You can quickly check whether an address is of a given kind by using the methods
* `isPayToPublicKeyHash` and `isPayToScriptTemplate`
*
* @example
* ```javascript
* // validate that an input field is valid
* let error = Address.getValidationError(input, 'testnet');
* if (!error) {
* let address = new Address(input, 'testnet');
* } else {
* // invalid network or checksum (typo?)
* let message = error.messsage;
* }
*
* // get an address from a public key
* let address = Address.fromPublicKey(publicKey, 'testnet').toString();
* ```
*
* @param data The encoded data in various formats
* @param network The network: 'mainnet' (default) or 'testnet'
* @param type The type of address: 'P2ST' (default) or 'P2PKH' or 'GROUP'
* @returns A new valid and frozen instance of an Address
*/
constructor(data: Address | string | Uint8Array, network?: Networkish, type?: AddressType) {
ValidationUtils.validateArgument(data != null, 'First argument is required, please include address data.');
if (data instanceof Address) {
return data; // Immutable instance
}
if (typeof data === 'string') {
return Address.fromString(data);
}
ValidationUtils.validateArgument(BufferUtils.isBuffer(data), "data must be Address, string or Buffer");
Address.validateParams(network, type);
this.data = data;
this.network = networks.get(network) || networks.defaultNetwork;
this.type = type || AddressType.PayToScriptTemplate;
}
public static validateParams(network?: Networkish, type?: AddressType): void {
if (network && !networks.get(network)) {
throw new TypeError('Second argument must be "mainnet" or "testnet".');
}
if (type && (type !== AddressType.PayToScriptTemplate && type !== AddressType.GroupIdAddress && type !== AddressType.PayToPublicKeyHash)) {
throw new TypeError('Third argument must be "P2ST", "P2PKH" or "GROUP"');
}
}
/**
* @param address string
*
* @returns A new valid and frozen instance of an Address
*/
public static fromString(address: string): Address {
ValidationUtils.validateArgument(typeof address === 'string', 'parameter supplied is not a string.');
ValidationUtils.validateArgument(address.length > 34, 'Invalid Address string provided');
let parts = AddressFormatter.decode(address);
let network = networks.get(parts.prefix, 'prefix');
return new Address(parts.data, network, parts.type);
}
/**
* Will return a cashaddr representation of the address. Always return lower case
* Can be converted by the caller to uppercase is needed (still valid).
*
* @returns Nexa address
*/
public toString(): string {
return AddressFormatter.encode(this.network.prefix, this.type, this.data);
}
/**
* Will return a string formatted for the console
*
* @returns {string} Bitcoin address
*/
public inspect(): string {
return `<Address: ${this}, type: ${this.type}, network: ${this.network}>`;
}
/**
* Instantiate an address from an Object
*
* @param obj - A JSON object with keys: data, network and type
* @returns A new valid instance of an Address
*/
public static fromObject(obj: AddressDto): Address {
ValidationUtils.validateArgument(typeof obj?.data === 'string', 'Must provide a `data` property');
ValidationUtils.validateArgument(typeof obj?.type === 'string', 'Must provide a `type` property');
ValidationUtils.validateState(CommonUtils.isHexa(obj.data), `Unexpected data property, expected to be hex.`);
let hashBuffer = BufferUtils.hexToBuffer(obj.data);
return new Address(hashBuffer, obj.network, obj.type as AddressType);
}
/**
* @returns A plain object with the address information
*/
public toJSON(): AddressDto {
return {
data: BufferUtils.bufferToHex(this.data),
type: this.type,
network: this.network.toString()
};
}
public toObject = this.toJSON;
/**
* @return true if an address is of pay to public key hash type
*/
public isPayToPublicKeyHash(): boolean {
return this.type === AddressType.PayToPublicKeyHash;
}
/**
* @return true if an address is of pay to script template type
*/
public isPayToScriptTemplate(): boolean {
return this.type === AddressType.PayToScriptTemplate;
}
/**
* @return true if an address is a group token address
*/
public isGroupIdentifierAddress(): boolean {
return this.type === AddressType.GroupIdAddress;
}
/**
* Will return a validation error if exists
*
* @example
* ```javascript
* // a network mismatch error
* let error = Address.getValidationError('nexatest:nqtsq5g567x44x5g54t2wsxz60zwqmyks63rkrddl4stwnzu', 'testnet');
* ```
*
* @param data The encoded data
* @param network either a Network instance, 'mainnet', or 'testnet'
* @param type The type of address: 'P2ST' or 'GROUP' or 'P2PKH'
* @returns The corresponding error message
*/
public static getValidationError(data: string | Uint8Array, network?: Networkish, type?: AddressType): Error | undefined {
try {
if (typeof data === 'string') {
let addr = Address.fromString(data);
data = addr.data;
if (network && networks.get(network)?.prefix !== addr.network.prefix) {
throw new Error('Address has mismatched network type.');
}
network = addr.network;
if (type && type !== addr.type) {
throw new Error('Address has mismatched type.');
}
type = addr.type;
}
this.validateParams(network, type);
AddressFormatter.encode(networks.get(network)?.prefix || networks.defaultNetwork.prefix, type || AddressType.PayToScriptTemplate, data);
} catch (e) {
return e as Error;
}
}
/**
* Will return a boolean if an address is valid
*
* @example
* ```javascript
* assert(Address.isValid('nexa:nqtsq5g567x44x5g54t2wsxz60zwqmyks63rkrddsfq94pd2', 'mainned'));
* ```
*
* @param data The encoded data
* @param network either a Network instance, 'mainnet', or 'testnet'
* @param type The type of address: 'P2ST' or 'GROUP' or 'P2PKH'
* @returns true if valid
*/
public static isValid(data: string | Uint8Array, network?: Networkish, type?: AddressType): boolean {
return !Address.getValidationError(data, network, type);
}
/**
* Instantiate an address from a PublicKey instance
*
* @param pubkey the public key instance
* @param network either a Network instance, 'mainnet' or 'testnet'
* @param type address encoding type
* @returns A new valid and frozen instance of an Address
*/
public static fromPublicKey(pubkey: PublicKey, network?: Networkish, type?: AddressType): Address {
let data = this._transformPublicKey(pubkey, type);
return new Address(data, network, type);
}
/**
* Instantiate an address from a non grouped script template
*
* @param templateHash An instance of a template hash Uint8Array
* @param constraintHash An instance of a constraint hash Uint8Array
* @param visibleArgs An array of push-only args, or hex string represent script buffer, or Script with push args
* @param network either a Network instance, 'mainnet' or 'testnet'
* @returns A new valid and frozen instance of an Address
*/
public static fromScriptTemplate(templateHash: Uint8Array | Opcode, constraintHash: Uint8Array | Opcode, visibleArgs?: string | Script | ScriptElement[], network?: Networkish): Address {
let data = Address._transformScriptTemplate(templateHash, constraintHash, visibleArgs);
return new Address(data, network, AddressType.PayToScriptTemplate);
}
/**
* Internal function to transform a {@link PublicKey}
*/
private static _transformPublicKey(pubkey: PublicKey, type = AddressType.PayToScriptTemplate): Uint8Array {
ValidationUtils.validateArgument(pubkey instanceof PublicKey, 'Address must be an instance of PublicKey.')
if (type === AddressType.PayToPublicKeyHash) {
return Hash.sha256ripemd160(pubkey.toBuffer());
} else if (type === AddressType.PayToScriptTemplate) {
let constraint = Script.empty().add(pubkey.toBuffer());
let constraintHash = Hash.sha256ripemd160(constraint.toBuffer());
return Address._transformScriptTemplate(Opcode.OP_1, constraintHash);
} else {
throw new Error("type must be P2ST or P2PKH");
}
}
/**
* Internal function to transform a Script Template params
*/
private static _transformScriptTemplate(templateHash: Uint8Array | Opcode, constraintHash: Uint8Array | Opcode, visibleArgs?: string | Script | ScriptElement[]): Uint8Array {
if (templateHash != Opcode.OP_1 && !BufferUtils.isHashBuffer(templateHash)) {
throw new TypeError('templateHash supplied is not a hash buffer or well-known OP_1.');
}
if (constraintHash != Opcode.OP_FALSE && !BufferUtils.isHashBuffer(constraintHash)) {
throw new TypeError('constraintHash supplied is not a hash buffer or OP_FALSE.');
}
let scriptTemplate = Script.empty().add(Opcode.OP_FALSE).add(templateHash).add(constraintHash);
if (visibleArgs) {
if (Array.isArray(visibleArgs)) {
visibleArgs.forEach(arg => scriptTemplate.add(arg));
} else if (visibleArgs instanceof Script) {
scriptTemplate.add(visibleArgs);
} else if (CommonUtils.isHexa(visibleArgs)) {
scriptTemplate.add(Script.fromHex(visibleArgs));
}
}
return new BufferWriter().writeVarLengthBuf(scriptTemplate.toBuffer()).toBuffer();
}
/**
* Will return the transaction output type by the address type
*
* @param address as string
* @returns 1 - Template, 0 - otherwise
*/
public static getOutputType(address: string): number {
return this.fromString(address).getOutputType();
}
/**
* Will return the transaction output type by the address type
*
* @returns 1 - Template, 0 - otherwise
*/
public getOutputType(): number {
return this.isPayToScriptTemplate() ? 1 : 0;
}
}