@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
351 lines (322 loc) • 10.8 kB
JavaScript
import * as R from 'ramda'
import BigNumber from 'bignumber.js'
import {
assertedType,
decodeBase58Check,
decodeBase64Check,
encodeBase58Check, encodeBase64Check,
hash,
salt
} from '../../utils/crypto'
import { toBytes } from '../../utils/bytes'
import {
ID_TAG_PREFIX,
PREFIX_ID_TAG,
NAME_BID_RANGES,
NAME_BID_MAX_LENGTH,
NAME_FEE,
NAME_FEE_BID_INCREMENT,
NAME_BID_TIMEOUTS,
NAME_ID_KEY
} from './schema'
import { ceil } from '../../utils/bignumber'
/**
* JavaScript-based Transaction builder helper function's
* @module @aeternity/aepp-sdk/es/tx/builder/helpers
* @export TxBuilderHelper
* @example import { TxBuilderHelper } from '@aeternity/aepp-sdk'
*/
export const createSalt = salt
const base64Types = ['tx', 'st', 'ss', 'pi', 'ov', 'or', 'cb', 'cs', 'ba']
/**
* Build a contract public key
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {string} ownerId The public key of the owner account
* @param {number} nonce the nonce of the transaction
* @return {string} Contract public key
*/
export function buildContractId (ownerId, nonce) {
const ownerIdAndNonce = Buffer.from([...decode(ownerId, 'ak'), ...toBytes(nonce)])
const b2bHash = hash(ownerIdAndNonce)
return encode(b2bHash, 'ct')
}
/**
* Build hash
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} prefix Transaction hash prefix
* @param {Buffer} data Rlp encoded transaction buffer
* @param {Object} options
* @param {Boolean} options.raw
* @return {String} Transaction hash
*/
export function buildHash (prefix, data, options = {}) {
return options.raw ? hash(data) : encode(hash(data), prefix)
}
/**
* Build a oracle query id
* @function
* @function* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} senderId The public key of the sender account
* @param {Number} nonce the nonce of the transaction
* @param {Number} oracleId The oracle public key
* @return {string} Contract public key
*/
export function oracleQueryId (senderId, nonce, oracleId) {
function _int32 (val) {
const nonceBE = toBytes(val, true)
return Buffer.concat([Buffer.alloc(32 - nonceBE.length), nonceBE])
}
const b2bHash = hash(Buffer.from([...decode(senderId, 'ak'), ..._int32(nonce), ...decode(oracleId, 'ok')]))
return encode(b2bHash, 'oq')
}
/**
* Format the salt into a 64-byte hex string
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {number} salt
* @return {string} Zero-padded hex string of salt
*/
export function formatSalt (salt) {
return Buffer.from(salt.toString(16).padStart(64, '0'), 'hex')
}
/**
* Encode a domain name
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name Name to encode
* @return {String} `nm_` prefixed encoded domain name
*/
export function produceNameId (name) {
ensureNameValid(name)
return encode(hash(name.toLowerCase()), 'nm')
}
/**
* Generate the commitment hash by hashing the formatted salt and
* name, base 58 encoding the result and prepending 'cm_'
*
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @function commitmentHash
* @category async
* @rtype (name: String, salt?: String) => hash: Promise[String]
* @param {String} name - Name to be registered
* @param {Number} salt Random salt
* @return {String} Commitment hash
*/
export function commitmentHash (name, salt = createSalt()) {
ensureNameValid(name)
return `cm_${encodeBase58Check(hash(Buffer.concat([Buffer.from(name.toLowerCase()), formatSalt(salt)])))}`
}
/**
* Decode data using the default encoding/decoding algorithm
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {string} data An encoded and prefixed string (ex tx_..., sg_..., ak_....)
* @param {string} type Prefix of Transaction
* @return {Buffer} Buffer of decoded Base58check or Base64check data
*/
export function decode (data, type = '') {
if (!type) type = data.split('_')[0]
return base64Types.includes(type)
? decodeBase64Check(assertedType(data, type))
: decodeBase58Check(assertedType(data, type))
}
/**
* Encode data using the default encoding/decoding algorithm
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {Buffer|String} data An decoded data
* @param {string} type Prefix of Transaction
* @return {String} Encoded string Base58check or Base64check data
*/
export function encode (data, type) {
return `${type}_${base64Types.includes(type)
? encodeBase64Check(data)
: encodeBase58Check(data)}`
}
/**
* Utility function to create and _id type
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {string} hashId Encoded hash
* @return {Buffer} Buffer Buffer with ID tag and decoded HASh
*/
export function writeId (hashId) {
const prefix = hashId.slice(0, 2)
const idTag = PREFIX_ID_TAG[prefix]
if (!idTag) throw new Error(`Id tag for prefix ${prefix} not found.`)
return Buffer.from([...toBytes(idTag), ...decode(hashId, prefix)])
}
/**
* Utility function to read and _id type
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {Buffer} buf Data
* @return {String} Encoided hash string with prefix
*/
export function readId (buf) {
const tag = buf.readUIntBE(0, 1)
const prefix = ID_TAG_PREFIX[tag]
if (!prefix) throw new Error(`Prefix for id-tag ${tag} not found.`)
return encode(buf.slice(1, buf.length), prefix)
}
/**
* Utility function to convert int to bytes
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {Number|String|BigNumber} val Value
* @return {Buffer} Buffer Buffer from number(BigEndian)
*/
export function writeInt (val) {
return toBytes(val, true)
}
/**
* Utility function to convert bytes to int
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {Buffer} buf Value
* @return {String} Buffer Buffer from number(BigEndian)
*/
export function readInt (buf = Buffer.from([])) {
return BigNumber(buf.toString('hex'), 16).toString(10)
}
/**
* Helper function to build pointers for name update TX
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {Array} pointers - Array of pointers ([ { key: 'account_pubkey', id: 'ak_32klj5j23k23j5423l434l2j3423'} ])
* @return {Array} Serialized pointers array
*/
export function buildPointers (pointers) {
return pointers.map(
p => [
toBytes(p.key),
writeId(p.id)
]
)
}
/**
* Helper function to read pointers from name update TX
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {Array} pointers - Array of pointers ([ { key: 'account_pubkey', id: 'ak_32klj5j23k23j5423l434l2j3423'} ])
* @return {Array} Deserialize pointer array
*/
export function readPointers (pointers) {
return pointers.map(
([key, id]) => Object.assign({
key: key.toString(),
id: readId(id)
})
)
}
/**
* Ensure that name is valid
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {string} name
* @return void
* @throws Error
*/
export function ensureNameValid (name) {
if (!name || typeof name !== 'string') throw new Error('Name must be a string')
if (!name.endsWith('.chain')) throw new Error(`Name should end with .chain: ${name}`)
}
/**
* Is name valid
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {string} name
* @return Boolean
*/
export function isNameValid (name) {
try {
ensureNameValid(name)
return true
} catch (error) {
return false
}
}
/**
* What kind of a hash is this? If it begins with 'ak_' it is an
* account key, if with 'ok_' it's an oracle key.
*
* @param s - the hash.
* returns the type, or throws an exception if type not found.
*/
export function classify (s) {
if (!s.match(/^[a-z]{2}_.+/)) {
throw new Error('Not a valid hash')
}
const klass = s.substr(0, 2)
if (klass in NAME_ID_KEY) {
return NAME_ID_KEY[klass]
} else {
throw new Error(`Unknown class ${klass}`)
}
}
/**
* Validate name pointers array
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String[]} pointers Pointers array. Allowed values is: account(ak_), oracle(ok_), contract(ct_), channel(ch_)
* @return {Boolean}
*/
export function validatePointers (pointers = []) {
return !pointers
.find(p => !p || typeof p !== 'string' || !['ak', 'ok', 'ct', 'ch'].includes(p.split('_')[0]))
}
/**
* Get the minimum name fee for a domain
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} domain the domain name to get the fee for
* @return {String} the minimum fee for the domain auction
*/
export function getMinimumNameFee (domain) {
const nameLength = domain.replace('.chain', '').length
return NAME_BID_RANGES[nameLength >= NAME_BID_MAX_LENGTH ? NAME_BID_MAX_LENGTH : nameLength]
}
/**
* Compute bid fee for AENS auction
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} domain the domain name to get the fee for
* @param {Number|String} startFee Auction start fee
* @param {Number} [increment=0.5] Bid multiplier(In percentage, must be between 0 and 1)
* @return {String} Bid fee
*/
export function computeBidFee (domain, startFee = NAME_FEE, increment = NAME_FEE_BID_INCREMENT) {
if (!(Number(increment) === increment && increment % 1 !== 0)) throw new Error(`Increment must be float. Current increment ${increment}`)
if (increment < NAME_FEE_BID_INCREMENT) throw new Error(`minimum increment percentage is ${NAME_FEE_BID_INCREMENT}`)
return ceil(
BigNumber(BigNumber(startFee).eq(NAME_FEE) ? getMinimumNameFee(domain) : startFee).times(BigNumber(NAME_FEE_BID_INCREMENT).plus(1))
)
}
/**
* Compute auction end height
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} domain the domain name to get the fee for
* @param {Number|String} claimHeight Auction starting height
* @return {String} Auction end height
*/
export function computeAuctionEndBlock (domain, claimHeight) {
return R.cond([
[R.lt(5), R.always(NAME_BID_TIMEOUTS[4].plus(claimHeight))],
[R.lt(9), R.always(NAME_BID_TIMEOUTS[8].plus(claimHeight))],
[R.lte(NAME_BID_MAX_LENGTH), R.always(NAME_BID_TIMEOUTS[12].plus(claimHeight))],
[R.T, R.always(BigNumber(claimHeight))]
])(domain.replace('.chain', '').length).toString(10)
}
/**
* Is name accept going to auction
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name Transaction abiVersion
* @return {Boolean}
*/
export function isAuctionName (name) {
return name.replace('.chain', '').length < 13
}