/
helpers.js
364 lines (337 loc) · 11.2 KB
/
helpers.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
import BigNumber from 'bignumber.js'
import {
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_FEE_BID_INCREMENT,
NAME_BID_TIMEOUTS,
NAME_MAX_LENGTH_FEE,
POINTER_KEY_BY_PREFIX
} from './schema'
import { ceil } from '../../utils/bignumber'
import {
PrefixMismatchError,
DecodeError,
EncodeError,
PayloadLengthError,
TagNotFoundError,
PrefixNotFoundError,
InvalidNameError,
IllegalBidFeeError,
NoDefaultAensPointerError,
IllegalArgumentError
} from '../../utils/errors'
/**
* 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
/**
* 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 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 an AENS name
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name Name to encode
* @return {String} `nm_` prefixed encoded AENS 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)])))}`
}
// based on https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
const base64Types = ['ba', 'cb', 'or', 'ov', 'pi', 'ss', 'cs', 'ck', 'cv', 'st', 'tx']
const base58Types = ['ak', 'bf', 'bs', 'bx', 'ch', 'cm', 'ct', 'kh', 'mh', 'nm', 'ok', 'oq', 'pp', 'sg', 'th']
// TODO: add all types with a fixed length
const typesLength = {
ak: 32,
ct: 32,
ok: 32
}
function ensureValidLength (data, type) {
if (!typesLength[type]) return
if (data.length === typesLength[type]) return
throw new PayloadLengthError(`Payload should be ${typesLength[type]} bytes, got ${data.length} instead`)
}
/**
* Decode data using the default encoding/decoding algorithm
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {string} data An Base58/64check encoded and prefixed string (ex tx_..., sg_..., ak_....)
* @param {string} [requiredPrefix] Ensure that data have this prefix
* @return {Buffer} Decoded data
*/
export function decode (data, requiredPrefix) {
if (typeof data !== 'string') throw new DecodeError(`Encoded should be a string, got ${data} instead`)
const [prefix, encodedPayload, extra] = data.split('_')
if (!encodedPayload) throw new DecodeError(`Encoded string missing payload: ${data}`)
if (extra) throw new DecodeError(`Encoded string have extra parts: ${data}`)
if (requiredPrefix && requiredPrefix !== prefix) {
throw new PrefixMismatchError(prefix, requiredPrefix)
}
const decoder = (base64Types.includes(prefix) && decodeBase64Check) ||
(base58Types.includes(prefix) && decodeBase58Check)
if (!decoder) {
throw new DecodeError(`Encoded string have unknown type: ${prefix}`)
}
const payload = decoder(encodedPayload)
ensureValidLength(payload, prefix)
return payload
}
/**
* 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) {
const encoder = (base64Types.includes(type) && encodeBase64Check) ||
(base58Types.includes(type) && encodeBase58Check)
if (!encoder) {
throw new EncodeError(`Unknown type: ${type}`)
}
ensureValidLength(data, type)
return `${type}_${encoder(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) {
if (typeof hashId !== 'string') {
throw new IllegalArgumentError(`Address should be a string, got ${hashId} instead`)
}
const prefix = hashId.slice(0, 2)
const idTag = PREFIX_ID_TAG[prefix]
if (!idTag) throw new TagNotFoundError(prefix)
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 PrefixNotFoundError(tag)
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)
})
)
}
const AENS_SUFFIX = '.chain'
/**
* Ensure that AENS 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 InvalidNameError('Name must be a string')
if (!name.endsWith(AENS_SUFFIX)) throw new InvalidNameError(`Name should end with ${AENS_SUFFIX}: ${name}`)
}
/**
* Is AENS 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
}
}
/**
* @param identifier - account/oracle/contract address, or channel
* @returns {String} default AENS pointer key
* @throws exception when default key not defined
*/
export function getDefaultPointerKey (identifier) {
decode(identifier)
const prefix = identifier.substr(0, 2)
return POINTER_KEY_BY_PREFIX[prefix] ||
(() => { throw new NoDefaultAensPointerError(prefix) })()
}
/**
* Get the minimum AENS name fee
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name the AENS name to get the fee for
* @return {String} the minimum fee for the AENS name auction
*/
export function getMinimumNameFee (name) {
ensureNameValid(name)
const nameLength = name.length - AENS_SUFFIX.length
return NAME_BID_RANGES[Math.min(nameLength, NAME_MAX_LENGTH_FEE)]
}
/**
* Compute bid fee for AENS auction
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name the AENS 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 (name, startFee, increment = NAME_FEE_BID_INCREMENT) {
if (!(Number(increment) === increment && increment % 1 !== 0)) throw new IllegalBidFeeError(`Increment must be float. Current increment ${increment}`)
if (increment < NAME_FEE_BID_INCREMENT) throw new IllegalBidFeeError(`minimum increment percentage is ${NAME_FEE_BID_INCREMENT}`)
return ceil(
BigNumber(startFee ?? getMinimumNameFee(name)).times(BigNumber(NAME_FEE_BID_INCREMENT).plus(1))
)
}
/**
* Compute auction end height
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name
* @param {Number|String} claimHeight Auction starting height
* @return {String} Auction end height
*/
export function computeAuctionEndBlock (name, claimHeight) {
ensureNameValid(name)
const length = name.length - AENS_SUFFIX.length
const h = (length <= 4 && NAME_BID_TIMEOUTS[4]) ||
(length <= 8 && NAME_BID_TIMEOUTS[8]) ||
(length <= 12 && NAME_BID_TIMEOUTS[12]) ||
NAME_BID_TIMEOUTS[13]
return h.plus(claimHeight).toString(10)
}
/**
* Is name accept going to auction
* @function
* @alias module:@aeternity/aepp-sdk/es/tx/builder/helpers
* @param {String} name
* @return {Boolean}
*/
export function isAuctionName (name) {
ensureNameValid(name)
return name.length < 13 + AENS_SUFFIX.length
}