lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
282 lines (281 loc) • 10.3 kB
JavaScript
import { Preconditions } from './util/preconditions.js';
import { Base58 } from './encoding/base58.js';
import { BufferWriter } from './encoding/bufferwriter.js';
import { Networks } from './networks.js';
import { Hash } from './crypto/hash.js';
import { JSUtil } from './util/js.js';
import { BufferUtil } from './util/buffer.js';
const TOKEN_NAME = 'lotus';
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
export class XAddress {
static PayToPublicKeyHash = 'pubkeyhash';
static PayToScriptHash = 'scripthash';
static PayToTaproot = 'taproot';
prefix;
hashBuffer;
network;
type;
constructor(data, network, type, prefix = TOKEN_NAME) {
if (data instanceof XAddress) {
return data;
}
Preconditions.checkArgument(data !== undefined, 'data', 'First argument is required, please include address data.', 'guide/address.html');
const networkExplicitlyProvided = network !== undefined;
network ||= Networks.defaultNetwork.name;
if (network && !Networks.get(network)) {
throw new TypeError('Second argument must be "livenet", "testnet", or "regtest".');
}
if (type &&
type !== XAddress.PayToPublicKeyHash &&
type !== XAddress.PayToScriptHash &&
type !== XAddress.PayToTaproot) {
throw new TypeError('Third argument must be "pubkeyhash", "scripthash", or "taproot".');
}
const info = this._classifyArguments(data, network, type, prefix, networkExplicitlyProvided);
info.network =
info.network || Networks.get(network) || Networks.defaultNetwork;
info.type = info.type || type || XAddress.PayToPublicKeyHash;
JSUtil.defineImmutable(this, {
prefix: info.prefix,
hashBuffer: info.hashBuffer,
network: info.network,
type: info.type,
});
}
_classifyArguments(data, network, type, prefix, networkExplicitlyProvided = true) {
if (typeof data === 'string') {
return XAddress._transformString(data, networkExplicitlyProvided ? network : undefined, type);
}
else if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
return XAddress._transformBuffer(data, network, type, prefix);
}
else if (typeof data === 'object' && data !== null) {
return XAddress._transformObject(data);
}
else {
throw new TypeError('First argument is an unrecognized data format.');
}
}
static _transformObject(data) {
Preconditions.checkArgument(data.hashBuffer !== undefined, 'data', 'Must provide a `hash` or `hashBuffer` property');
Preconditions.checkArgument(data.type !== undefined, 'data', 'Must provide a `type` property');
return {
hashBuffer: data.hashBuffer || Buffer.from(data.hashBuffer.toString(), 'hex'),
network: Networks.get(data.network) || Networks.defaultNetwork,
type: data.type,
prefix: data.prefix,
};
}
static _classifyFromVersion(buffer) {
const version = {};
const pubkeyhashNetwork = Networks.get(buffer[0], 'pubkeyhash');
const scripthashNetwork = Networks.get(buffer[0], 'scripthash');
if (pubkeyhashNetwork) {
version.network = pubkeyhashNetwork;
version.type = XAddress.PayToPublicKeyHash;
}
else if (scripthashNetwork) {
version.network = scripthashNetwork;
version.type = XAddress.PayToScriptHash;
}
return version;
}
static _transformString(data, network, type) {
if (typeof data !== 'string') {
throw new TypeError('data parameter supplied is not a string.');
}
data = data.trim();
const networkObj = Networks.get(network);
if (network && !networkObj) {
throw new TypeError('Unknown network');
}
const info = XAddress._decode(data);
if (!info.network ||
(networkObj && networkObj.name !== info.network.name)) {
throw new TypeError('Address has mismatched network type.');
}
return info;
}
static _transformBuffer(buffer, network, type, prefix = TOKEN_NAME) {
const info = {};
if (!Buffer.isBuffer(buffer) && !(buffer instanceof Uint8Array)) {
throw new TypeError('XAddress supplied is not a buffer.');
}
const networkObj = Networks.get(network);
if (network && !networkObj) {
throw new TypeError('Unknown network');
}
if (type === undefined) {
throw new TypeError('Unknown type.');
}
info.prefix = prefix;
info.hashBuffer = Buffer.from(buffer);
info.network = networkObj || Networks.defaultNetwork;
info.type = type;
return info;
}
static fromString(str, network, type) {
const info = XAddress._transformString(str, network, type);
return new XAddress(info.hashBuffer, info.network, info.type);
}
static fromObject(obj) {
Preconditions.checkState(JSUtil.isHexa(obj.hash), 'Unexpected hash property, "' + obj.hash + '", expected to be hex.');
const hashBuffer = Buffer.from(obj.hash, 'hex');
return new XAddress(hashBuffer, obj.network, obj.type, obj.prefix);
}
static getValidationError(data, network, type) {
try {
new XAddress(data, network, type);
return null;
}
catch (e) {
return e;
}
}
static isValid(data, network, type) {
return !XAddress.getValidationError(data, network, type);
}
static _decode(address) {
return decode(address);
}
toBuffer() {
const version = Buffer.from([
this.network[this.type],
]);
const buf = Buffer.concat([version, this.hashBuffer]);
return buf;
}
toObject() {
return {
prefix: this.prefix,
hash: this.hashBuffer.toString('hex'),
type: this.type,
network: this.network.toString(),
};
}
toJSON() {
return this.toObject();
}
toXAddress() {
const prefix = this.prefix;
const networkChar = getNetworkChar(this.network);
const networkByte = Buffer.from(networkChar);
const typeByte = Buffer.from([getTypeByte(this.type)]);
const payload = this.hashBuffer;
const checksum = createChecksum(prefix, networkByte, typeByte, payload);
const encodedPayload = encodePayload(typeByte, payload, checksum);
return prefix + networkChar + encodedPayload;
}
toString() {
return this.toXAddress();
}
isPayToPublicKeyHash() {
return this.type === XAddress.PayToPublicKeyHash;
}
isPayToScriptHash() {
return this.type === XAddress.PayToScriptHash;
}
isPayToTaproot() {
return this.type === XAddress.PayToTaproot;
}
inspect() {
return '<XAddress: ' + this.toString() + ', type: ' + this.type + '>';
}
}
function createChecksum(prefix, networkByte, typeByte, payload) {
const data = BufferUtil.concat([
Buffer.from(prefix),
networkByte,
typeByte,
payload,
]);
return Hash.sha256(data).subarray(0, 4);
}
function createChecksumLegacy(prefix, networkByte, typeByte, payload) {
const bw = new BufferWriter();
bw.writeVarintNum(prefix.length);
bw.write(Buffer.from(prefix));
bw.writeUInt8(networkByte[0]);
bw.writeUInt8(typeByte[0]);
bw.writeVarintNum(payload.length);
bw.write(payload);
const buf = bw.concat();
return Hash.sha256(buf).subarray(0, 4);
}
function getType(typeByte) {
switch (typeByte) {
case 0:
return 'pubkeyhash';
case 1:
return 'scripthash';
case 2:
return 'taproot';
}
return 'pubkeyhash';
}
function getTypeByte(type) {
switch (type) {
case 'pubkeyhash':
case 'scripthash':
return 0;
case 'taproot':
return 2;
}
return 0;
}
function getNetworkFromChar(networkChar) {
switch (networkChar) {
case '_':
return Networks.get('livenet');
case 'T':
return Networks.get('testnet');
case 'R':
return Networks.get('regtest');
default:
throw new TypeError('Unknown network type: ' + networkChar);
}
}
function getNetworkChar(network) {
if (network.name === 'livenet') {
return '_';
}
else if (network.name === 'testnet') {
return 'T';
}
else if (network.name === 'regtest') {
return 'R';
}
else {
throw new TypeError('Unknown network: ' + network.name);
}
}
function encodePayload(typeByte, payload, checksum) {
const bw = new BufferWriter();
bw.writeUInt8(typeByte[0]);
bw.write(payload);
bw.write(checksum);
const buf = bw.concat();
return Base58.encode(buf);
}
function decode(address) {
const match = /[A-Z]|_/.exec(address);
const splitLocation = match ? match.index : 0;
const prefix = address.substring(0, splitLocation);
const networkChar = address.substring(splitLocation, splitLocation + 1);
const networkByte = Buffer.from(networkChar);
const encodedPayload = address.substring(splitLocation + 1);
const decodedBytes = Base58.decode(encodedPayload);
const typeByte = decodedBytes.subarray(0, 1);
const payload = decodedBytes.subarray(1, decodedBytes.length - 4);
const decodedChecksum = decodedBytes.subarray(decodedBytes.length - 4);
const checksum = createChecksum(prefix, networkByte, typeByte, payload);
const legacyChecksum = createChecksumLegacy(prefix, networkByte, typeByte, payload);
Preconditions.checkArgument(checksum.toString('hex') === decodedChecksum.toString('hex') ||
legacyChecksum.toString('hex') === decodedChecksum.toString('hex'), 'checksum', 'Invalid checksum: ' + address);
const info = {};
info.hashBuffer = payload;
info.network = getNetworkFromChar(networkChar);
info.type = getType(typeByte[0]);
info.prefix = prefix;
return info;
}