@logosnetwork/logos-webwallet-sdk
Version:
Create Logos wallets with or without a full Logos node
1,218 lines (1,141 loc) • 41.3 kB
text/typescript
import bigInt from 'big-integer'
import Account, { AccountJSON, AccountOptions } from './Account'
import { Settings as RpcSettings, Privileges as RpcPrivileges, Request as RpcRequest } from '@logosnetwork/logos-rpc-client/api'
import {
accountFromHexKey,
keyFromAccount,
GENESIS_HASH,
deserializeControllers,
deserializeSettings,
serializeController,
MAXUINT128
} from './Utils/Utils'
import {
Send,
Issuance,
IssueAdditional,
ChangeSetting,
ImmuteSetting,
Revoke,
AdjustUserStatus,
AdjustFee,
UpdateIssuerInfo,
UpdateController,
Burn,
Distribute,
WithdrawFee,
WithdrawLogos,
TokenSend,
TokenRequest,
Request
} from './Requests'
import { Setting } from './Requests/ChangeSetting'
type feeType = 'flat'|'percentage'
export interface TokenAccountJSON extends AccountJSON {
tokenID?: string;
tokenBalance?: string;
totalSupply?: string;
tokenFeeBalance?: string;
symbol?: string;
name?: string;
issuerInfo?: string;
feeRate?: string;
feeType?: feeType;
accountStatuses?: AccountStatuses;
controllers?: Controller[];
settings?: Settings;
type?: string;
}
interface AccountStatuses {
[address: string]: {
whitelisted: boolean;
frozen: boolean;
};
}
interface AccountStatus {
whitelisted: boolean;
frozen: boolean;
}
export interface SyncedResponse {
account?: string;
synced?: boolean;
type?: string;
remove?: boolean;
}
export interface Privileges {
change_issuance: boolean;
change_modify_issuance: boolean;
change_revoke: boolean;
change_modify_revoke: boolean;
change_freeze: boolean;
change_modify_freeze: boolean;
change_adjust_fee: boolean;
change_modify_adjust_fee: boolean;
change_whitelist: boolean;
change_modify_whitelist: boolean;
issuance: boolean;
revoke: boolean;
freeze: boolean;
adjust_fee: boolean;
whitelist: boolean;
update_issuer_info: boolean;
update_controller: boolean;
burn: boolean;
distribute: boolean;
withdraw_fee: boolean;
withdraw_logos: boolean;
}
export interface Controller {
account?: string;
privileges?: Privileges;
}
export interface Settings {
issuance: boolean;
modify_issuance: boolean;
revoke: boolean;
modify_revoke: boolean;
freeze: boolean;
modify_freeze: boolean;
adjust_fee: boolean;
modify_adjust_fee: boolean;
whitelist: boolean;
modify_whitelist: boolean;
}
export interface TokenAccountOptions extends AccountOptions {
tokenID?: string;
issuance?: Issuance;
tokenBalance?: string;
totalSupply?: string;
tokenFeeBalance?: string;
symbol?: string;
name?: string;
issuerInfo?: string;
feeRate?: string;
feeType?: feeType;
controllers?: Controller[];
settings?: Settings;
accountStatuses?: AccountStatuses;
}
/**
* TokenAccount contain the keys, chains, and balances.
*/
export default class TokenAccount extends Account {
private _tokenBalance: string
private _totalSupply: string
private _tokenFeeBalance: string
private _symbol: string
private _name: string
private _issuerInfo: string
private _feeRate: string
private _feeType: feeType
private _controllers: Controller[]
private _settings: Settings
private _accountStatuses: AccountStatuses
// private _pendingTokenBalance: string
// private _pendingTotalSupply: string
public constructor (options: TokenAccountOptions) {
if (!options) throw new Error('You must pass settings to initalize the token account')
if (!options.address && !options.tokenID) throw new Error('You must initalize a token account with an address or tokenID')
if (!options.wallet) throw new Error('You must initalize a token account with a wallet')
if (options.tokenID !== undefined) {
options.publicKey = options.tokenID
options.address = accountFromHexKey(options.tokenID)
} else if (options.address !== undefined) {
options.publicKey = keyFromAccount(options.address)
}
super(options)
if (options.issuance !== undefined && options.issuance !== null) {
this._tokenBalance = options.issuance.totalSupply
this._totalSupply = options.issuance.totalSupply
this._tokenFeeBalance = '0'
this._symbol = options.issuance.symbol
this._name = options.issuance.name
this._issuerInfo = options.issuance.issuerInfo
this._feeRate = options.issuance.feeRate
this._feeType = options.issuance.feeType
this._controllers = options.issuance.controllersAsObject
this._settings = options.issuance.settingsAsObject
}
/**
* Token Balance of the token account
*
* @type {string}
* @private
*/
if (options.tokenBalance !== undefined) {
this._tokenBalance = options.tokenBalance
} else {
this._tokenBalance = '0'
}
/**
* Total Supply of tokens
*
* @type {string}
* @private
*/
if (options.totalSupply !== undefined) {
this._totalSupply = options.totalSupply
} else {
this._totalSupply = null
}
/**
* Token Fee Balance
*
* @type {string}
* @private
*/
if (options.tokenFeeBalance !== undefined) {
this._tokenFeeBalance = options.tokenFeeBalance
} else {
this._tokenFeeBalance = '0'
}
/**
* Symbol of the token
*
* @type {string}
* @private
*/
if (options.symbol !== undefined) {
this._symbol = options.symbol
} else {
this._symbol = null
}
/**
* Name of the token
*
* @type {string}
* @private
*/
if (options.name !== undefined) {
this._name = options.name
} else {
this._name = 'Unknown Token'
}
/**
* Issuer Info of the token
* @type {string}
* @private
*/
if (options.issuerInfo !== undefined) {
this._issuerInfo = options.issuerInfo
} else {
this._issuerInfo = null
}
/**
* Fee Rate of the token
*
* @type {string}
* @private
*/
if (options.feeRate !== undefined) {
this._feeRate = options.feeRate
} else {
this._feeRate = null
}
/**
* Fee Type of the token
*
* @type {string}
* @private
*/
if (options.feeType !== undefined) {
this._feeType = options.feeType
} else {
this._feeType = null
}
/**
* Controllers of the token
*
* @type {string}
* @private
*/
if (options.controllers !== undefined) {
this._controllers = options.controllers
} else {
this._controllers = null
}
/**
* Settings of the token
* @type {Settings}
* @private
*/
if (options.settings !== undefined) {
this._settings = options.settings
} else {
this._settings = {
issuance: null,
modify_issuance: null,
revoke: null,
modify_revoke: null,
freeze: null,
modify_freeze: null,
adjust_fee: null,
modify_adjust_fee: null,
whitelist: null,
modify_whitelist: null
}
}
/**
* Account Statuses
*
* @type {AccountStatuses}
*/
if (options.accountStatuses !== undefined) {
this._accountStatuses = options.accountStatuses
} else {
this._accountStatuses = {}
}
}
/**
* The type of the account (LogosAccount or TokenAccount)
* @type {string}
*/
public get type (): 'TokenAccount' { return 'TokenAccount' }
/**
* The public key of the token account
* @type {string}
* @readonly
*/
public get tokenID (): string {
return this.publicKey
}
/**
* The accounts statuses (Frozen / Whitelisted)
* @type {string}
* @readonly
*/
public get accountStatuses (): AccountStatuses {
return this._accountStatuses
}
public set accountStatuses (statuses: AccountStatuses) {
this._accountStatuses = statuses
}
/**
* The balance of the token in the minor token unit
* @type {string}
* @readonly
*/
public get tokenBalance (): string {
return this._tokenBalance
}
public set tokenBalance (val: string) {
this._tokenBalance = val
}
/**
* The total supply of the token in minor token
* @type {string}
* @readonly
*/
public get totalSupply (): string {
return this._totalSupply
}
public set totalSupply (val: string) {
this._totalSupply = val
}
/**
* The total supply of the token in the minor token unit
* @type {string}
* @readonly
*/
public get tokenFeeBalance (): string {
return this._tokenFeeBalance
}
public set tokenFeeBalance (val: string) {
this._tokenFeeBalance = val
}
/**
* The issuer info of the token
* @type {string}
*/
public get issuerInfo (): string {
return this._issuerInfo
}
public set issuerInfo (val: string) {
this._issuerInfo = val
}
/**
* The symbol of the token
* @type {string}
*/
public get symbol (): string {
return this._symbol
}
public set symbol (val: string) {
this._symbol = val
}
/**
* The name of the token
* @type {string}
*/
public get name (): string {
return this._name
}
public set name (val: string) {
this._name = val
}
/**
* The fee rate of the token
* @type {string}
*/
public get feeRate (): string {
return this._feeRate
}
public set feeRate (val: string) {
this._feeRate = val
}
/**
* The fee type of the token
* @type {feeType}
*/
public get feeType (): feeType {
return this._feeType
}
public set feeType (val: feeType) {
this._feeType = val
}
/**
* The settings of the token
* @type {Settings}
*/
public get settings (): Settings {
return this._settings
}
public set settings (val: Settings) {
this._settings = val
}
/**
* The controllers of the token
* @type {Controller[]}
*/
public get controllers (): Controller[] {
return this._controllers
}
public set controllers (val: Controller[]) {
this._controllers = val
}
/**
* The decimals of the token
* @type {number}
*/
public get decimals (): number {
try {
const parsedInfo = JSON.parse(this.issuerInfo)
if (parsedInfo &&
typeof parsedInfo.decimals !== 'undefined' &&
parsedInfo.decimals > 0) {
return parseInt(parsedInfo.decimals)
}
return null
} catch (e) {
return null
}
}
public convertToMajor (minorValue: string): string {
if (this.decimals) return this.wallet.rpcClient.convert.fromTo(minorValue, 0, this.decimals)
return null
}
public convertToMinor (majorValue: string): string {
if (this.decimals) return this.wallet.rpcClient.convert.fromTo(majorValue, this.decimals, 0)
return null
}
/**
* Checks if the account is synced
* @returns {Promise<SyncedResponse>}
*/
public isSynced (): Promise<SyncedResponse> {
return new Promise((resolve): void => {
const RPC = this.wallet.rpcClient
RPC.accounts.info(this.address).then(async (info): Promise<void> => {
let synced = true
if (info && info.frontier) {
this.tokenBalance = info.token_balance
this.totalSupply = info.total_supply
this.tokenFeeBalance = info.token_fee_balance
this.symbol = info.symbol
this.name = info.name
this.issuerInfo = info.issuer_info
this.feeRate = info.fee_rate
this.feeType = info.fee_type
this.controllers = deserializeControllers(info.controllers)
this.settings = deserializeSettings(info.settings)
this.balance = info.balance
if (info.frontier !== GENESIS_HASH) {
if (this.chain.length === 0 || this.chain[this.chain.length - 1].hash !== info.frontier) {
synced = false
}
}
if (synced) {
const receiveBlock = await RPC.requests.info(info.receive_tip)
if (this.receiveChain.length === 0 || this.receiveChain[this.receiveChain.length - 1].hash !== receiveBlock.send_hash) {
synced = false
}
}
if (synced) {
if (this.wallet.validateSync) {
if (this.verifyChain() && this.verifyReceiveChain()) {
this.synced = synced
console.info(`${info.name} has been fully synced and validated`)
resolve({ account: this.address, synced: this.synced, type: 'TokenAccount' })
}
} else {
console.info('Finished Syncing: Requests were not validated')
this.synced = synced
resolve({ account: this.address, synced: this.synced, type: 'TokenAccount' })
}
} else {
this.synced = synced
resolve({ account: this.address, synced: this.synced, type: 'TokenAccount' })
}
} else {
if (this.receiveChain.length === 0 && this.chain.length === 0) {
console.info(`${this.address} is empty and therefore valid`)
this.synced = synced
resolve({ account: this.address, synced: this.synced, type: 'TokenAccount' })
} else {
console.error(`${this.address} is not opened according to the RPC. This is a critical error if in a production enviroment. On testnet this just means the network has been restarted.`)
this.synced = false
resolve({ account: this.address, synced: this.synced, type: 'TokenAccount', remove: true })
}
}
})
})
}
/**
* Scans the account history using RPC and updates the local chains
* @returns {Promise<Account>}
*/
public sync (): Promise<Account> {
return new Promise((resolve): void => {
this.synced = false
this.chain = []
this.receiveChain = []
const RPC = this.wallet.rpcClient
RPC.accounts.info(this.address).then((info): void => {
if (!info || !info.type || info.type !== 'TokenAccount') {
throw new Error('Invalid Address - This is not a valid token account')
}
this.tokenBalance = info.token_balance
this.totalSupply = info.total_supply
this.tokenFeeBalance = info.token_fee_balance
this.symbol = info.symbol
this.name = info.name
this.issuerInfo = info.issuer_info
this.feeRate = info.fee_rate
this.feeType = info.fee_type
this.controllers = deserializeControllers(info.controllers)
this.settings = deserializeSettings(info.settings)
this.balance = info.balance
if (this.wallet.fullSync) {
RPC.accounts.history(this.address, -1, true).then((history): void => {
if (history) {
// Add Genesis to latest
for (const requestInfo of history.reverse()) {
const request = this.addConfirmedRequest(requestInfo)
if (request instanceof AdjustUserStatus) {
this.updateAccountStatusFromRequest(request)
}
}
if (this.wallet.validateSync) {
if (this.verifyChain() && this.verifyReceiveChain()) {
this.synced = true
console.info(`${info.name} has been fully synced and validated`)
resolve(this)
}
} else {
console.info('Finished Syncing: Requests were not validated')
this.synced = true
resolve(this)
}
} else {
this.synced = true
console.info(`${this.address} is empty and therefore valid`)
resolve(this)
}
})
} else {
if (info && info.frontier && info.frontier !== GENESIS_HASH) {
RPC.requests.info(info.frontier).then((val): void => {
const request = this.addConfirmedRequest(val)
if (request !== null && !request.verify()) {
throw new Error(`Invalid Request from RPC sync! \n ${JSON.stringify(request.toJSON(), null, 2)}`)
}
this.synced = true
console.info(`${info.name} has been lazy synced`)
resolve(this)
})
} else {
this.synced = true
console.info(`${this.address} is empty and therefore valid`)
resolve(this)
}
}
})
})
}
/**
* Updates the token account by comparing the RPC token account info with the changes in a new request
* Also updates the pending balance based on the new balance and the pending chain
* @param {Request} request - request that is being calculated on
* @returns {void}
*/
public updateTokenInfoFromRequest (request: Request): void {
if (request instanceof IssueAdditional) {
this.totalSupply = bigInt(this.totalSupply).plus(bigInt(request.amount)).toString()
this.tokenBalance = bigInt(this.tokenBalance).plus(bigInt(request.amount)).toString()
} else if (request instanceof ChangeSetting) {
this.settings[request.setting] = request.value
} else if (request instanceof ImmuteSetting) {
this.settings[`modify_${request.setting}`] = false
} else if (request instanceof Revoke) {
if (request.transaction.destination === this.address) {
this.tokenBalance = bigInt(this.tokenBalance).plus(bigInt(request.transaction.amount)).toString()
}
// Handle if TK account is SRC?
} else if (request instanceof AdjustUserStatus) {
this.updateAccountStatusFromRequest(request)
} else if (request instanceof AdjustFee) {
this.feeRate = request.feeRate
this.feeType = request.feeType
} else if (request instanceof UpdateIssuerInfo) {
this.issuerInfo = request.issuerInfo
} else if (request instanceof UpdateController) {
const updatedPrivs = serializeController(request.controller).privileges
if (request.action === 'remove' && updatedPrivs.length === 0) {
this.controllers = this.controllers.filter((controller): boolean => controller.account !== request.controller.account)
} else if (request.action === 'remove' && updatedPrivs.length > 0) {
for (const controller of this.controllers) {
if (controller.account === request.controller.account) {
for (const priv of updatedPrivs) {
controller.privileges[priv] = false
}
}
}
} else if (request.action === 'add') {
if (this.controllers.some((controller): boolean => controller.account === request.controller.account)) {
for (const controller of this.controllers) {
if (controller.account === request.controller.account) {
for (const priv of updatedPrivs) {
controller.privileges[priv] = true
}
}
}
} else {
this.controllers.push(request.controller)
}
}
} else if (request instanceof Burn) {
this.totalSupply = bigInt(this.totalSupply).minus(bigInt(request.amount)).toString()
this.tokenBalance = bigInt(this.tokenBalance).minus(bigInt(request.amount)).toString()
} else if (request instanceof Distribute) {
this.tokenBalance = bigInt(this.tokenBalance).minus(bigInt(request.transaction.amount)).toString()
} else if (request instanceof WithdrawFee) {
this.tokenFeeBalance = bigInt(this.tokenFeeBalance).minus(bigInt(request.transaction.amount)).toString()
} else if (request instanceof WithdrawLogos) {
if (request.tokenID === this.tokenID) {
this.balance = bigInt(this.balance).minus(bigInt(request.transaction.amount)).minus(bigInt(request.fee)).toString()
}
if (request.transaction.destination === this.address) {
this.balance = bigInt(this.balance).plus(bigInt(request.transaction.amount)).toString()
}
} else if (request instanceof Send) {
for (const transaction of request.transactions) {
if (transaction.destination === this.address) {
this.balance = bigInt(this.balance).plus(bigInt(transaction.amount)).toString()
}
}
} else if (request instanceof Issuance) {
this.tokenBalance = request.totalSupply
// this._pendingTokenBalance = request.totalSupply
this.totalSupply = request.totalSupply
// this._pendingTotalSupply = request.totalSupply
this.tokenFeeBalance = '0'
this.symbol = request.symbol
this.name = request.name
this.issuerInfo = request.issuerInfo
this.feeRate = request.feeRate
this.feeType = request.feeType
this.controllers = request.controllersAsObject
this.settings = request.settingsAsObject
this.balance = '0'
this.pendingBalance = '0'
} else if (request.type === 'token_send') {
if (request.tokenFee) {
this.tokenFeeBalance = bigInt(this.tokenFeeBalance).plus(request.tokenFee).toString()
}
}
if (request.type !== 'send' && request.type !== 'issuance' &&
request.type !== 'token_send' && request.type !== 'withdraw_logos') {
this.balance = bigInt(this.balance).minus(bigInt(request.fee)).toString()
}
}
/**
* Validates if the token account contains the controller
*
* @param {string} address - Address of the logos account you are checking if they are a controller
* @returns {boolean}
*/
public isController (address: string): boolean {
for (const controller of this.controllers) {
if (controller.account === address) {
return true
}
}
return false
}
/**
* Validates if the token has the setting
*
* @param {Setting} setting - Token setting you are checking
* @returns {boolean}
*/
public hasSetting (setting: RpcSettings): boolean {
return Boolean(this.settings[setting])
}
/**
* Validates if the token account contains the controller and the controller has the specified privilege
*
* @param {string} address - Address of the controller you are checking
* @param {privilege} privilege - Privilege you are checking for
* @returns {boolean}
*/
public controllerPrivilege (address: string, privilege: RpcPrivileges): boolean {
for (const controller of this.controllers) {
if (controller.account === address) {
return controller.privileges[privilege]
}
}
return false
}
/**
* Validates if the account has enough token funds to complete the transaction
*
* @param {string} address - Address of the controller you are checking
* @param {string} amount - Amount you are checking for
* @returns {Promise<boolean>}
*/
public async accountHasFunds (address: string, amount: string): Promise<boolean> {
if (!this.wallet.rpc) {
console.warn('Cannot client-side validate if an account has funds without RPC enabled')
return true
} else {
const RPC = this.wallet.rpcClient
const info = await RPC.accounts.info(address)
return bigInt(info.tokens[this.tokenID].balance).greaterOrEquals(bigInt(amount))
}
}
/**
* Validates if the account is a valid destination to send token funds to
*
* @param {string} address - Address of the controller you are checking
* @returns {Promise<boolean>}
*/
public async validTokenDestination (address: string): Promise<boolean> {
// TODO 104 - This token account is a valid destiantion
if (!this.wallet.rpc) {
console.warn('Cannot client-side validate destination without RPC enabled')
return true
} else {
const RPC = this.wallet.rpcClient
const info = await RPC.accounts.info(address)
if (info.type !== 'LogosAccount') return false
let tokenInfo = null
if (info && info.tokens && info.tokens[this.tokenID]) {
tokenInfo = info.tokens[this.tokenID]
}
if (!tokenInfo && this.hasSetting('whitelist')) {
return false
} else if (!tokenInfo && !this.hasSetting('whitelist')) {
return true
} else if (this.hasSetting('whitelist') && tokenInfo.whitelisted === 'false') {
return false
} else if (tokenInfo.frozen === 'true') {
return false
} else {
return true
}
}
}
/**
* Validates that the account has enough funds at the current time to publish the request
*
* @param {Request} request - Request information from the RPC or MQTT
* @returns {Promise<boolean>}
*/
public async validateRequest (request: Request): Promise<boolean> {
if (bigInt(this.balance).minus(request.fee).lesser(0)) {
console.error('Invalid Request: Token Account does not have enough Logos to afford the fee perform token opperation')
return false
} else {
if (request instanceof IssueAdditional) {
if (bigInt(this.totalSupply).plus(bigInt(request.amount)).greater(bigInt(MAXUINT128))) {
console.error('Invalid Issue Additional Request: Total Supply would exceed MAXUINT128')
return false
} else if (!this.hasSetting('issuance')) {
console.error('Invalid Issue Additional Request: Token does not allow issuance')
return false
} else if (!this.controllerPrivilege(request.originAccount, 'issuance')) {
console.error('Invalid Issue Additional Request: Controller does not have permission to issue additional tokens')
return false
} else {
return true
}
} else if (request instanceof ChangeSetting) {
if (!this.hasSetting(this.settingToModify(request.setting))) {
console.error(`Invalid Change Setting Request: ${this.name} does not allow changing ${request.setting}`)
return false
} else if (!this.controllerPrivilege(request.originAccount, this.settingToChange(request.setting))) {
console.error(`Invalid Change Setting Request: Controller does not have permission to change ${request.setting}`)
return false
} else {
return true
}
} else if (request instanceof ImmuteSetting) {
if (!this.hasSetting(this.settingToModify(request.setting))) {
console.error(`Invalid Immute Setting Request: ${request.setting} is already immuatable`)
return false
} else if (!this.controllerPrivilege(request.originAccount, this.settingToChangeModify(request.setting))) {
console.error(`Invalid Immute Setting Request: Controller does not have permission to immute ${request.setting}`)
return false
} else {
return true
}
} else if (request instanceof Revoke) {
if (!this.hasSetting('revoke')) {
console.error(`Invalid Revoke Request: ${this.name} does not support revoking accounts`)
return false
} else if (!this.controllerPrivilege(request.originAccount, 'revoke')) {
console.error('Invalid Revoke Request: Controller does not have permission to issue revoke requests')
return false
} else if (await !this.accountHasFunds(request.source, request.transaction.amount)) {
console.error(`Invalid Revoke Request: Source account does not have sufficient ${this.symbol} to complete this request`)
return false
} else if (await !this.validTokenDestination(request.transaction.destination)) {
console.error(`Invalid Revoke Request: Destination does not have permission to receive ${this.symbol}`)
return false
} else {
return true
}
} else if (request instanceof AdjustUserStatus) {
if (request.status === 'frozen' || request.status === 'unfrozen') {
if (!this.hasSetting('freeze')) {
console.error(`Invalid Adjust User Status: ${this.name} does not support freezing accounts`)
return false
} else if (!this.controllerPrivilege(request.originAccount, 'freeze')) {
console.error('Invalid Adjust User Status Request: Controller does not have permission to freeze accounts')
return false
} else {
return true
}
} else if (request.status === 'whitelisted' || request.status === 'not_whitelisted') {
if (!this.hasSetting('whitelist')) {
console.error(`Invalid Adjust User Status: ${this.name} does not require whitelisting accounts`)
return false
} else if (!this.controllerPrivilege(request.originAccount, 'revoke')) {
console.error('Invalid Adjust User Status Request: Controller does not have permission to whitelist accounts')
return false
} else {
return true
}
} else {
console.error(`Invalid Adjust User Status: ${request.status} is not a valid status`)
return false
}
} else if (request instanceof AdjustFee) {
if (!this.hasSetting('adjust_fee')) {
console.error(`Invalid Adjust Fee Request: ${this.name} does not allow changing the fee type or fee rate`)
return false
} else if (!this.controllerPrivilege(request.originAccount, 'adjust_fee')) {
console.error('Invalid Adjust Fee Request: Controller does not have permission to freeze accounts')
return false
} else {
return true
}
} else if (request instanceof UpdateIssuerInfo) {
if (!this.controllerPrivilege(request.originAccount, 'update_issuer_info')) {
console.error('Invalid Update Issuer Info Request: Controller does not have permission to update the issuer info')
return false
} else {
return true
}
} else if (request instanceof UpdateController) {
if (!this.controllerPrivilege(request.originAccount, 'update_controller')) {
console.error('Invalid Update Controller Request: Controller does not have permission to update controllers')
return false
} else if (this.controllers.length === 10 && request.action === 'add' && !this.isController(request.controller.account)) {
console.error(`Invalid Update Controller Request: ${this.name} already has 10 controllers you must remove one first`)
return false
} else {
return true
}
} else if (request instanceof Burn) {
if (!this.controllerPrivilege(request.originAccount, 'burn')) {
console.error('Invalid Burn Request: Controller does not have permission to burn tokens')
return false
} else if (bigInt(this.tokenBalance).lesser(bigInt(request.amount))) {
console.error('Invalid Burn Request: the token balance of the token account is less than the amount of tokens you are trying to burn')
return false
} else {
return true
}
} else if (request instanceof Distribute) {
if (!this.controllerPrivilege(request.originAccount, 'distribute')) {
console.error('Invalid Distribute Request: Controller does not have permission to distribute tokens')
return false
} else if (bigInt(this.tokenBalance).lesser(bigInt(request.transaction.amount))) {
console.error(`Invalid Distribute Request: Token account does not have sufficient ${this.symbol} to distribute`)
return false
} else if (await !this.validTokenDestination(request.transaction.destination)) {
console.error(`Invalid Distribute Request: Destination does not have permission to receive ${this.symbol}`)
return false
} else {
return true
}
} else if (request instanceof WithdrawFee) {
if (!this.controllerPrivilege(request.originAccount, 'withdraw_fee')) {
console.error('Invalid Withdraw Fee Request: Controller does not have permission to withdraw fee')
return false
} else if (bigInt(this.tokenFeeBalance).lesser(bigInt(request.transaction.amount))) {
console.error('Invalid Withdraw Fee Request: Token account does not have a sufficient token fee balance to withdraw the specified amount')
return false
} else if (await !this.validTokenDestination(request.transaction.destination)) {
console.error(`Invalid Withdraw Fee Request: Destination does not have permission to receive ${this.symbol}`)
return false
} else {
return true
}
} else if (request instanceof WithdrawLogos) {
if (!this.controllerPrivilege(request.originAccount, 'withdraw_logos')) {
console.error('Invalid Withdraw Logos Request: Controller does not have permission to withdraw logos')
return false
} else if (bigInt(this.balance).lesser(bigInt(request.transaction.amount).plus(bigInt(request.fee)))) {
console.error('Invalid Withdraw Logos Request: Token account does not have sufficient balance to withdraw the specified amount + the minimum logos fee')
return false
} else {
return true
}
} else {
return false
}
}
}
private settingToModify (setting: Setting): RpcSettings {
if (setting === 'issuance') {
return 'modify_issuance'
} else if (setting === 'revoke') {
return 'modify_revoke'
} else if (setting === 'adjust_fee') {
return 'modify_adjust_fee'
} else if (setting === 'freeze') {
return 'modify_freeze'
} else if (setting === 'whitelist') {
return 'modify_whitelist'
}
return null
}
private settingToChange (setting: Setting): RpcPrivileges {
if (setting === 'issuance') {
return 'change_issuance'
} else if (setting === 'revoke') {
return 'change_revoke'
} else if (setting === 'adjust_fee') {
return 'change_adjust_fee'
} else if (setting === 'freeze') {
return 'change_freeze'
} else if (setting === 'whitelist') {
return 'change_whitelist'
}
return null
}
private settingToChangeModify (setting: Setting): RpcPrivileges {
if (setting === 'issuance') {
return 'change_modify_issuance'
} else if (setting === 'revoke') {
return 'change_modify_revoke'
} else if (setting === 'adjust_fee') {
return 'change_modify_adjust_fee'
} else if (setting === 'freeze') {
return 'change_modify_freeze'
} else if (setting === 'whitelist') {
return 'change_modify_whitelist'
}
return null
}
/**
* Adds a request to the appropriate chain
*
* @param {RequestOptions} requestInfo - Request information from the RPC or MQTT
* @returns {Request}
*/
public addConfirmedRequest (requestInfo: RpcRequest): Request {
let request = null
if (requestInfo.type === 'send') {
const request = new Send(requestInfo)
if (requestInfo.transactions && requestInfo.transactions.length > 0) {
for (const trans of requestInfo.transactions) {
if (trans.destination === this.address) {
this.addToReceiveChain(request)
break
}
}
}
return request
} else if (requestInfo.type === 'withdraw_logos') {
request = new WithdrawLogos(requestInfo)
if (requestInfo.transaction.destination === this.address) {
this.addToReceiveChain(request)
}
if (requestInfo.token_id === this.tokenID) {
this.addToSendChain(request)
}
return request
} else if (requestInfo.type === 'issue_additional') {
request = new IssueAdditional(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'change_setting') {
request = new ChangeSetting(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'immute_setting') {
request = new ImmuteSetting(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'revoke') {
request = new Revoke(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'adjust_user_status') {
request = new AdjustUserStatus(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'adjust_fee') {
request = new AdjustFee(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'update_issuer_info') {
request = new UpdateIssuerInfo(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'update_controller') {
request = new UpdateController(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'burn') {
request = new Burn(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'distribute') {
request = new Distribute(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'withdraw_fee') {
request = new WithdrawFee(requestInfo)
this.addToSendChain(request)
return request
} else if (requestInfo.type === 'issuance') {
request = new Issuance(requestInfo)
this.addToReceiveChain(request)
return request
} else if (requestInfo.type === 'token_send') {
request = new TokenSend(requestInfo)
return request
} else {
console.error(`MQTT sent ${this.name} an unknown block type: ${requestInfo.type} hash: ${requestInfo.hash}`)
return null
}
}
/**
* Returns the status of the given address for this token
*
* @param {string} address - The address of the account
* @returns {AccountStatus} status of the account { whitelisted and frozen }
*/
public getAccountStatus (address: string): AccountStatus {
if (Object.prototype.hasOwnProperty.call(this.accountStatuses, address)) {
return this.accountStatuses[address]
} else {
return {
whitelisted: false,
frozen: false
}
}
}
/**
* Returns the status of the given address for this token
*
* @param {AdjustUserStatus} request - The adjust_user_status request
* @returns {AccountStatus} status of the account { whitelisted and frozen }
*/
public updateAccountStatusFromRequest (request: AdjustUserStatus): AccountStatus {
if (!this.accountStatuses[request.account]) {
this.accountStatuses[request.account] = {
frozen: false,
whitelisted: false
}
}
if (request.status === 'frozen') {
this.accountStatuses[request.account].frozen = true
} else if (request.status === 'unfrozen') {
this.accountStatuses[request.account].frozen = false
} else if (request.status === 'whitelisted') {
this.accountStatuses[request.account].whitelisted = true
} else if (request.status === 'not_whitelisted') {
this.accountStatuses[request.account].whitelisted = false
}
return this.accountStatuses[request.account]
}
/**
* Confirms the request in the local chain
*
* @param {MQTTRequestOptions} requestInfo The request from MQTT
* @throws An exception if the request is not found in the pending requests array
* @throws An exception if the previous request does not match the last chain request
* @throws An exception if the request amount is greater than your balance minus the transaction fee
* @returns {Promise<void>}
*/
public async processRequest (requestInfo: RpcRequest): Promise<void> {
// Confirm the requests / updates balances / broadcasts next block
const request = this.addConfirmedRequest(requestInfo)
if (request !== null) {
if (!request.verify()) throw new Error(`Invalid Request! \n ${JSON.stringify(request.toJSON(), null, 2)}`)
// Todo 104 - revoke, token_send, distribute, withdraw_Fee, withdraw_logos
// could be recieved by TokenAccount???
if (request instanceof TokenRequest &&
request.tokenID === this.tokenID &&
request instanceof TokenSend === false) {
if (this.getPendingRequest(requestInfo.hash)) {
this.removePendingRequest(requestInfo.hash)
} else {
console.error('Someone is performing token account requests that is not us!!!')
// Remove all pendings as they are now invalidated
// It is possible to update the pending blocks but this could
// lead to unintended consequences so its best to just reset IMO
this.removePendingRequests()
}
}
this.updateTokenInfoFromRequest(request)
this.broadcastRequest()
}
}
/**
* Returns the token account JSON
* @returns {TokenAccountJSON} JSON request
*/
public toJSON (): TokenAccountJSON {
const obj: TokenAccountJSON = super.toJSON()
obj.tokenID = this.tokenID
obj.tokenBalance = this.tokenBalance
obj.totalSupply = this.totalSupply
obj.tokenFeeBalance = this.tokenFeeBalance
obj.symbol = this.symbol
obj.name = this.name
obj.issuerInfo = this.issuerInfo
obj.feeRate = this.feeRate
obj.feeType = this.feeType
obj.accountStatuses = this.accountStatuses
obj.controllers = this.controllers
obj.settings = this.settings
obj.type = this.type
return obj
}
}