@logosnetwork/logos-webwallet-sdk
Version:
Create Logos wallets with or without a full Logos node
401 lines (356 loc) • 9.62 kB
text/typescript
import { keyFromAccount, hexToUint8, uint8ToHex, decToHex, changeEndianness, GENESIS_HASH, EMPTY_WORK } from '../Utils/Utils'
import Blake2b from '../Utils/blake2b'
import nacl from 'tweetnacl/nacl'
import Logos, { LogosConstructorOptions } from '@logosnetwork/logos-rpc-client'
import { RPCOptions } from '../Wallet'
export interface RequestOptions {
origin?: string;
previous?: string;
sequence?: number | string;
fee?: string;
signature?: string;
timestamp?: string;
work?: string;
type?: RequestType | string;
}
interface RequestType {
text: string;
value: number;
}
export interface RequestJSON {
previous?: string;
sequence?: number;
origin?: string;
fee?: string;
work?: string;
hash?: string;
type?: string;
signature?: string;
timestamp?: string;
}
/**
* The base class for all Requests.
*/
export default abstract class Request {
private _signature: string
private _work: string
private _previous: string
private _fee: string
private _origin: string
private _sequence: number
private _timestamp: string
private _version: number
private _published: boolean
private _type: RequestType
public constructor (options: RequestOptions = {
origin: null,
previous: null,
sequence: null,
fee: null,
signature: null,
timestamp: null,
work: EMPTY_WORK,
type: null
}) {
/**
* Signature of the request
* @type {string}
* @private
*/
if (options.signature !== undefined) {
this._signature = options.signature
} else {
this._signature = null
}
/**
* Work of the request based on previous hash
* @type {Hexadecimal16Length}
* @private
*/
if (options.work !== undefined) {
this._work = options.work
} else {
this._work = EMPTY_WORK
}
/**
* Previous request hash
* @type {string}
* @private
*/
if (options.previous !== undefined) {
this._previous = options.previous
} else {
this._previous = null
}
/**
* Fee of the request
* @type {string}
* @private
*/
if (options.fee !== undefined) {
this._fee = options.fee
} else {
this._fee = null
}
/**
* Logos account address of the request origin account
* @type {string}
* @private
*/
if (options.origin !== undefined) {
this._origin = options.origin
} else {
this._origin = null
}
/**
* Sequence of the request in the chain
* @type {number}
* @private
*/
if (options.sequence !== undefined) {
this._sequence = parseInt(options.sequence.toString())
} else {
this._sequence = null
}
/**
* Timestamp of the request (this might not be perfectly accurate)
* @type {string}
* @private
*/
if (options.timestamp !== undefined) {
this._timestamp = options.timestamp
} else {
this._timestamp = null
}
/**
* Type of the request
* @type {RequestType}
* @private
*/
if (options.type !== undefined) {
this._type = options.type as RequestType
} else {
this._type = null
}
/**
* Request version of webwallet SDK
* @type {number}
* @private
*/
this._version = 1
this._published = false
}
public get published (): boolean {
return this._published
}
public set published (val: boolean) {
this._published = val
}
/**
* Return the signature of the request
* @type {string}
* @readonly
*/
public get signature (): string {
return this._signature
}
public set signature (hex: string) {
this._signature = hex
}
/**
* Return the work of the request
* @type {string}
*/
public get work (): string {
return this._work
}
public set work (hex: string) {
if (!this._previous) throw new Error('Previous is not set.')
this._work = hex
}
/**
* Return the previous request as hash
* @type {string}
*/
public get previous (): string {
return this._previous
}
public set previous (hex: string) {
if (!/[0-9A-F]{64}/i.test(hex)) throw new Error('Invalid previous request hash.')
this._previous = hex
}
/**
* Return the string amount of the fee in reason
* @type {string}
*/
public get fee (): string {
return this._fee
}
public set fee (val: string) {
this._fee = val
}
/**
* Return the the sequence of the request in the origin account
* @type {number}
*/
public get sequence (): number {
return this._sequence
}
public set sequence (val: number) {
this._sequence = val
}
/**
* Return the the timestamp of when the request was confirmed
* @type {number}
*/
public get timestamp (): string {
return this._timestamp
}
public set timestamp (timestamp: string) {
this._timestamp = timestamp
}
/**
* The origin account public key
* @type {string}
* @readonly
*/
public get origin (): string {
return keyFromAccount(this._origin)
}
public set origin (origin: string) {
this._origin = origin
}
/**
* The origin account address
* @type {string}
* @readonly
*/
public get originAccount (): string {
return this._origin
}
/**
* Returns the type of this request
* @type {string}
* @readonly
*/
public get type (): string {
if (!this._type) return null
return this._type.text
}
/**
* Returns the type value of this request
* @type {number}
* @readonly
*/
public get typeValue (): number {
if (!this._type) return null
return this._type.value
}
/**
* Returns the version of this request
* @type {number}
* @readonly
*/
public get version (): number {
return this._version
}
/**
* Returns a hash for the request
* @returns {string} - Hash
*/
public abstract get hash (): string
/**
* Creates a signature for the request
* @param {string} privateKey - private key in hex
* @returns {boolean} if the signature is valid
*/
public sign (privateKey: string): boolean {
const uint8Key: Uint8Array = hexToUint8(privateKey)
if (uint8Key.length !== 32) throw new Error('Invalid Private Key length. Should be 32 bytes.')
const hash = hexToUint8(this.hash)
this.signature = uint8ToHex(nacl.sign.detached(hash, uint8Key))
return this.verify()
}
/**
* Creates a Blake2b Context for the request
* @returns {context} - Blake2b Context
*/
public requestHash (): Blake2b {
if (!this.previous) throw new Error('Previous is not set.')
if (this.sequence === null) throw new Error('Sequence is not set.')
if (this.fee === null) throw new Error('Transaction fee is not set.')
if (!this.origin) throw new Error('Origin account is not set.')
if (!this.type) throw new Error('Request type is not defined.')
return new Blake2b()
.update(hexToUint8(decToHex(this.typeValue, 1)))
.update(hexToUint8(this.origin))
.update(hexToUint8(this.previous))
.update(hexToUint8(decToHex(this.fee, 16)))
.update(hexToUint8(changeEndianness(decToHex(this.sequence, 4))))
}
/**
* Verifies the request's integrity
* @returns {boolean}
*/
public verify (): boolean {
if (!this.hash) throw new Error('Hash is not set.')
if (!this.signature) throw new Error('Signature is not set.')
if (!this.origin) throw new Error('Origin account is not set.')
return nacl.sign.detached.verify(hexToUint8(this.hash), hexToUint8(this.signature), hexToUint8(this.origin))
}
/**
* Publishes the request
* @param {string[]} delegates - current delegates
* @returns {Promise<{hash:string}>} response of transcation publish
*/
public async publish (rpcSettings: RPCOptions, delegates: string[] = null): Promise<{hash: string}> {
let delegateId = null
if (delegates !== null) {
if (this.previous !== GENESIS_HASH) {
delegateId = parseInt(this.previous.slice(-2), 16) % 32
} else {
delegateId = parseInt(this.origin.slice(-2), 16) % 32
}
}
let rpcOptions: LogosConstructorOptions = null
if (delegateId !== null) {
rpcOptions = {
url: `http://${delegates[delegateId]}:${rpcSettings.nodePort}`
}
if (rpcSettings.proxy) rpcOptions.proxyURL = rpcSettings.proxy
console.info(`Publishing ${this.type} ${this.sequence} to Delegate ${delegateId}`)
} else {
rpcOptions = {
url: `http://${rpcSettings.nodeURL}:${rpcSettings.nodePort}`
}
if (rpcSettings.proxy) rpcOptions.proxyURL = rpcSettings.proxy
console.info(`Publishing ${this.type} ${this.sequence}`)
}
const RPC = new Logos(rpcOptions)
const response = await RPC.requests.publish(JSON.stringify(this.toJSON()))
if (response.hash) {
console.info(`Request accepted ${this.type} ${this.sequence}`)
return response
} else {
console.error(`Invalid Request: Rejected \n ${JSON.stringify(response)}`)
throw new Error(`Invalid Request: Rejected \n ${JSON.stringify(response)}`)
}
}
/**
* Returns the base request JSON
* @returns {RequestJSON} RequestJSON as string
*/
public toJSON (): RequestJSON {
const obj: RequestJSON = {}
obj.previous = this.previous
obj.sequence = this.sequence
obj.origin = this._origin
obj.fee = this.fee
obj.work = this.work
obj.hash = this.hash
obj.type = this.type
obj.signature = this.signature
if (this.timestamp) obj.timestamp = this.timestamp
return obj
}
}