UNPKG

ton3-core

Version:
400 lines (343 loc) 11.2 kB
import { crc16BytesBe } from './utils/checksum' import { bytesToBase64, base64ToBytes, hexToBytes, bytesToHex, bytesCompare } from './utils/helpers' const FLAG_BOUNCEABLE = 0x11 const FLAG_NON_BOUNCEABLE = 0x51 const FLAG_TEST_ONLY = 0x80 export type AddressType = 'base64' | 'raw' interface AddressTag { bounceable: boolean testOnly: boolean } interface AddressData extends AddressTag { workchain: number hash: Uint8Array } interface AddressRewriteOptions { workchain?: number bounceable?: boolean testOnly?: boolean } interface AddressStringifyOptions extends AddressRewriteOptions { urlSafe?: boolean } /** * Smart contract address * * @class Address */ class Address { private readonly _hash: Uint8Array private readonly _workchain: number private readonly _bounceable: boolean private readonly _testOnly: boolean /** * Creates an instance of {@link Address} * * Next formats can be used as constructor argument: * - Raw * - Base64 * - Address * * @param {(string | Address | Uint8Array)} address * @param {AddressRewriteOptions} [options] - Rewrite original address workchain and flags * * @example * ```ts * import { Address } from 'ton3-core' * * const address = new Address('kf/8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15+KsQHFLbKSMiYIny') * const rewrite = { workchain: 0, bounceable: true, testOnly: true } * * new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260') * new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260', rewrite) * new Address('kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny') * new Address(address) * ``` */ constructor (address: string | Address, options?: AddressRewriteOptions) { const isAddress = Address.isAddress(address) const isEncoded = Address.isEncoded(address) const isRaw = Address.isRaw(address) let result: AddressData switch (true) { case isAddress: result = Address.parseAddress(address as Address) break case isEncoded: result = Address.parseEncoded(address as string) break case isRaw: result = Address.parseRaw(address as string) break default: result = null break } if (result === null) { throw new Error('Address: can\'t parse address. Unknown type.') } const { workchain = result.workchain, bounceable = result.bounceable, testOnly = result.testOnly } = options || {} this._hash = result.hash this._workchain = workchain this._bounceable = bounceable this._testOnly = testOnly } /** * Get parsed {@link Address} hash part * * @example * ```ts * import { Address, Utils } from 'ton3-core' * * const address = new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260') * const hash = address.hash // Uint8Array * * console.log(Utils.Helpers.bytesToHex(hash)) * // fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260 * ``` * * @returns {Uint8Array} */ public get hash (): Uint8Array { return new Uint8Array(this._hash) } /** * Get parsed {@link Address} workchain * * @example * ```ts * import { Address } from 'ton3-core' * * const address = new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260') * * console.log(address.workchain) * // -1 * ``` * * @returns {number} */ public get workchain (): number { return this._workchain } /** * Get parsed {@link Address} bounceable flag * * @example * ```ts * import { Address } from 'ton3-core' * * const address = new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260') * * console.log(address.bounceable) * // false * ``` * * @returns {boolean} */ public get bounceable (): boolean { return this._bounceable } /** * Get parsed {@link Address} testOnly flag * * @example * ```ts * import { Address } from 'ton3-core' * * const address = new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260') * * console.log(address.testOnly) * // false * ``` * * @returns {boolean} */ public get testOnly (): boolean { return this._testOnly } private static isEncoded (address: any): boolean { // eslint-disable-next-line no-useless-escape const re = /^([a-zA-Z0-9_-]{48}|[a-zA-Z0-9\/\+]{48})$/ return typeof address === 'string' && re.test(address) } private static isRaw (address: any): boolean { const re = /^-?[0-9]:[a-zA-Z0-9]{64}$/ return typeof address === 'string' && re.test(address) } private static parseEncoded (value: string): AddressData { const base64 = value.replace(/-/g, '+').replace(/_/g, '/') const bytes = base64ToBytes(base64) const data = Array.from(bytes) const address = data.splice(0, 34) const checksum = data.splice(0, 2) const crc = crc16BytesBe(address) if (!bytesCompare(crc, checksum)) { throw new Error('Address: can\'t parse address. Wrong checksum.') } const { buffer } = new Uint8Array(address.splice(0, 2)) const view = new DataView(buffer) const tag = view.getUint8(0) const workchain = view.getInt8(1) const hash = new Uint8Array(address.splice(0, 32)) const { bounceable, testOnly } = Address.decodeTag(tag) return { bounceable, testOnly, workchain, hash } } private static parseAddress (value: Address): AddressData { const { workchain, bounceable, testOnly } = value const hash = new Uint8Array(value.hash) return { bounceable, testOnly, workchain, hash } } private static parseRaw (value: string): AddressData { const data = value.split(':') const workchain = parseInt(data[0], 10) const hash = hexToBytes(data[1]) const bounceable = false const testOnly = false return { bounceable, testOnly, workchain, hash } } private static encodeTag (options: AddressTag): number { const { bounceable, testOnly } = options const tag = bounceable ? FLAG_BOUNCEABLE : FLAG_NON_BOUNCEABLE return testOnly ? (tag | FLAG_TEST_ONLY) : tag } private static decodeTag (tag: number): AddressTag { let data = tag const testOnly = (data & FLAG_TEST_ONLY) !== 0 if (testOnly) { data ^= FLAG_TEST_ONLY } if (![ FLAG_BOUNCEABLE, FLAG_NON_BOUNCEABLE ].includes(data)) { throw new Error('Address: bad address tag.') } const bounceable = data === FLAG_BOUNCEABLE return { bounceable, testOnly } } /** * Compare instances of {@link Address} for hash and workchain equality * * @param {Address} address - Instance of another {@link Address} * * @example * ```ts * import { Address } from 'ton3-core' * * const address1 = new Address('-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260') * const address2 = new Address('kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny') * * console.log(address1.eq(address2)) * // true * ``` * * @returns {boolean} */ public eq (address: Address): boolean { return address === this || (bytesCompare(this._hash, address.hash) && this._workchain === address.workchain) } /** * Get raw or base64 representation of {@link Address} * * @param {AddressType} [type="base64"] - Can be "base64" or "raw" * @param {AddressStringifyOptions} [options] - Url-safe representation (only works for base64), flags, workchain setup. * * @example * ```ts * import { Address } from 'ton3-core' * * const raw = '-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260' * const address = new Address(raw, { bounceable: true, testOnly: true }) * * console.log(address.toString('base64')) * // kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny * * console.log(address.toString('base64', { urlSafe: false })) * // kf/8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15+KsQHFLbKSMiYIny * * console.log(address.toString('raw', { workchain: 0 })) * // 0:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260 * ``` * * @returns {string} */ public toString (type: AddressType = 'base64', options?: AddressStringifyOptions): string { const { workchain = this.workchain, bounceable = this.bounceable, testOnly = this.testOnly, urlSafe = true } = options || {} if (typeof workchain !== 'number' || workchain < -128 || workchain >= 128) { throw new Error('Address: workchain must be int8.') } if (typeof bounceable !== 'boolean') { throw new Error('Address: bounceable flag must be a boolean.') } if (typeof testOnly !== 'boolean') { throw new Error('Address: testOnly flag must be a boolean.') } if (typeof urlSafe !== 'boolean') { throw new Error('Address: urlSafe flag must be a boolean.') } if (type === 'raw') { return `${workchain}:${bytesToHex(this._hash)}`.toUpperCase() } const tag = Address.encodeTag({ bounceable, testOnly }) const address = new Uint8Array([ tag, workchain, ...this._hash ]) const checksum = crc16BytesBe(address) const base64 = bytesToBase64(new Uint8Array([ ...address, ...checksum ])) return urlSafe ? base64.replace(/\//g, '_').replace(/\+/g, '-') : base64.replace(/_/g, '/').replace(/-/g, '+') } /** * Helper method for writing null addresses to {@link Builder} * * @static */ public static readonly NONE = null private static isAddress (address: any): boolean { return address instanceof Address } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types public static isValid (address: any): boolean { try { // eslint-disable-next-line no-new new Address(address) return true } catch (e) { return false } } } export { Address, AddressRewriteOptions, AddressStringifyOptions }