tonweb
Version:
TonWeb - JavaScript API for TON blockchain
490 lines (453 loc) • 16.4 kB
JavaScript
const {BitString} = require("./BitString");
const {
bytesToBase64,
compareBytes,
concatBytes,
crc32c,
hexToBytes,
readNBytesUIntFromArray,
sha256,
bytesToHex
} = require("../utils");
const {Slice} = require("./Slice");
const reachBocMagicPrefix = hexToBytes('B5EE9C72');
const leanBocMagicPrefix = hexToBytes('68ff65f3');
const leanBocMagicPrefixCRC = hexToBytes('acc3a728');
class Cell {
constructor() {
this.bits = new BitString(1023);
this.refs = [];
this.isExotic = false;
}
/**
* @param serializedBoc {string | Uint8Array} hex or bytearray
* @return {Cell[]} root cells
*/
static fromBoc(serializedBoc) {
return deserializeBoc(serializedBoc);
}
/**
* @param serializedBoc {string | Uint8Array} hex or bytearray
* @return {Cell} root cell
*/
static oneFromBoc(serializedBoc) {
const cells = deserializeBoc(serializedBoc);
if (cells.length !== 1) throw new Error('expected 1 root cell but have ' + cells.length);
return cells[0];
}
/**
* Write another cell to this cell
* @param anotherCell {Cell}
*/
writeCell(anotherCell) {
// XXX we do not check that there are anough place in cell
this.bits.writeBitString(anotherCell.bits);
this.refs = this.refs.concat(anotherCell.refs);
}
/**
* @return {number}
*/
getMaxLevel() {
//TODO level calculation differ for exotic cells
let maxLevel = 0;
for (let k in this.refs) {
const i = this.refs[k];
if (i.getMaxLevel() > maxLevel) {
maxLevel = i.getMaxLevel();
}
}
return maxLevel;
}
/**
* @return {number}
*/
isExplicitlyStoredHashes() {
return 0;
}
/**
* @return {number}
*/
getMaxDepth() {
let maxDepth = 0;
if (this.refs.length > 0) {
for (let k = 0; k < this.refs.length; k++) {
const child = this.refs[k];
let childMaxDepth = child.getMaxDepth();
if (childMaxDepth > maxDepth) {
maxDepth = childMaxDepth;
}
}
maxDepth = maxDepth + 1;
}
return maxDepth;
}
/**
* @private
* @return {Uint8Array}
*/
getMaxDepthAsArray() {
const maxDepth = this.getMaxDepth();
const d = Uint8Array.from({length: 2}, () => 0);
d[1] = maxDepth % 256;
d[0] = Math.floor(maxDepth / 256);
return d;
}
/**
* @return {Uint8Array}
*/
getRefsDescriptor() {
const d1 = Uint8Array.from({length: 1}, () => 0);
d1[0] = this.refs.length + this.isExotic * 8 + this.getMaxLevel() * 32;
return d1;
}
/**
* @return {Uint8Array}
*/
getBitsDescriptor() {
const d2 = Uint8Array.from({length: 1}, () => 0);
d2[0] = Math.ceil(this.bits.cursor / 8) + Math.floor(this.bits.cursor / 8);
return d2;
}
/**
* @return {Uint8Array}
*/
getDataWithDescriptors() {
const d1 = this.getRefsDescriptor();
const d2 = this.getBitsDescriptor();
const tuBits = this.bits.getTopUppedArray();
return concatBytes(concatBytes(d1, d2), tuBits);
}
/**
* @return {Promise<Uint8Array>}
*/
async getRepr() {
const reprArray = [];
reprArray.push(this.getDataWithDescriptors());
for (let k in this.refs) {
const i = this.refs[k];
reprArray.push(i.getMaxDepthAsArray());
}
for (let k in this.refs) {
const i = this.refs[k];
reprArray.push(await i.hash());
}
let x = new Uint8Array();
for (let k in reprArray) {
const i = reprArray[k];
x = concatBytes(x, i);
}
return x;
}
/**
* @return {Promise<Uint8Array>}
*/
async hash() {
return new Uint8Array(
await sha256(await this.getRepr())
);
}
beginParse() {
const refs = this.refs.map(ref => ref.beginParse());
return new Slice(this.bits.array.slice(), this.bits.length, refs);
}
/**
* Recursively prints cell's content like Fift
* @return {string}
*/
print(indent) {
indent = indent || '';
let s = indent + 'x{' + this.bits.toHex() + '}\n';
for (let k in this.refs) {
const i = this.refs[k];
s += i.print(indent + ' ');
}
return s;
}
//serialized_boc#b5ee9c72 has_idx:(## 1) has_crc32c:(## 1)
// has_cache_bits:(## 1) flags:(## 2) { flags = 0 }
// size:(## 3) { size <= 4 }
// off_bytes:(## 8) { off_bytes <= 8 }
// cells:(##(size * 8))
// roots:(##(size * 8)) { roots >= 1 }
// absent:(##(size * 8)) { roots + absent <= cells }
// tot_cells_size:(##(off_bytes * 8))
// root_list:(roots * ##(size * 8))
// index:has_idx?(cells * ##(off_bytes * 8))
// cell_data:(tot_cells_size * [ uint8 ])
// crc32c:has_crc32c?uint32
// = BagOfCells;
/**
* create boc bytearray
* @param has_idx? {boolean}
* @param hash_crc32? {boolean}
* @param has_cache_bits? {boolean}
* @param flags? {number}
* @return {Promise<Uint8Array>}
*/
async toBoc(has_idx = true, hash_crc32 = true, has_cache_bits = false, flags = 0) {
const root_cell = this;
const allcells = await root_cell.treeWalk();
const topologicalOrder = allcells[0];
const cellsIndex = allcells[1];
const cells_num = topologicalOrder.length;
const s = cells_num.toString(2).length; // Minimal number of bits to represent reference (unused?)
const s_bytes = Math.min(Math.ceil(s / 8), 1);
let full_size = 0;
let sizeIndex = [];
for (let cell_info of topologicalOrder) {
//TODO it should be async map or async for
sizeIndex.push(full_size);
full_size = full_size + await cell_info[1].bocSerializationSize(cellsIndex, s_bytes);
}
const offset_bits = full_size.toString(2).length; // Minimal number of bits to offset/len (unused?)
const offset_bytes = Math.max(Math.ceil(offset_bits / 8), 1);
const serialization = new BitString((1023 + 32 * 4 + 32 * 3) * topologicalOrder.length);
serialization.writeBytes(reachBocMagicPrefix);
serialization.writeBitArray([has_idx, hash_crc32, has_cache_bits]);
serialization.writeUint(flags, 2);
serialization.writeUint(s_bytes, 3);
serialization.writeUint8(offset_bytes);
serialization.writeUint(cells_num, s_bytes * 8);
serialization.writeUint(1, s_bytes * 8); // One root for now
serialization.writeUint(0, s_bytes * 8); // Complete BOCs only
serialization.writeUint(full_size, offset_bytes * 8);
serialization.writeUint(0, s_bytes * 8); // Root shoulh have index 0
if (has_idx) {
topologicalOrder.forEach(
(cell_data, index) =>
serialization.writeUint(sizeIndex[index], offset_bytes * 8));
}
for (let cell_info of topologicalOrder) {
//TODO it should be async map or async for
const refcell_ser = await cell_info[1].serializeForBoc(cellsIndex, s_bytes);
serialization.writeBytes(refcell_ser);
}
let ser_arr = serialization.getTopUppedArray();
if (hash_crc32) {
ser_arr = concatBytes(ser_arr, crc32c(ser_arr));
}
return ser_arr;
}
/**
* @private
* @param cellsIndex
* @param refSize
* @return {Promise<Uint8Array>}
*/
async serializeForBoc(cellsIndex, refSize) {
const reprArray = [];
reprArray.push(this.getDataWithDescriptors());
if (this.isExplicitlyStoredHashes()) {
throw new Error("Cell hashes explicit storing is not implemented");
}
for (let k in this.refs) {
const i = this.refs[k];
const refHash = await i.hash();
const refIndexInt = cellsIndex[refHash];
let refIndexHex = refIndexInt.toString(16);
if (refIndexHex.length % 2) {
refIndexHex = "0" + refIndexHex;
}
const reference = hexToBytes(refIndexHex);
reprArray.push(reference);
}
let x = new Uint8Array();
for (let k in reprArray) {
const i = reprArray[k];
x = concatBytes(x, i);
}
return x;
}
/**
* @private
* @param cellsIndex
* @param refSize
* @return {Promise<number>}
*/
async bocSerializationSize(cellsIndex, refSize) {
return (await this.serializeForBoc(cellsIndex, refSize)).length;
}
/**
* @private
* @return {[[], {}]} topologicalOrderArray and indexHashmap
*/
async treeWalk() {
return treeWalk(this, [], {});
}
}
async function moveToTheEnd(indexHashmap, topologicalOrderArray, target) {
const targetIndex = indexHashmap[target];
for (let h in indexHashmap) {
if (indexHashmap[h] > targetIndex) {
indexHashmap[h] = indexHashmap[h] - 1;
}
}
indexHashmap[target] = topologicalOrderArray.length - 1;
const data = topologicalOrderArray.splice(targetIndex, 1)[0];
topologicalOrderArray.push(data);
for (let subCell of data[1].refs) {
await moveToTheEnd(indexHashmap, topologicalOrderArray, await subCell.hash());
}
}
/**
* @param cell {Cell}
* @param topologicalOrderArray array of pairs: cellHash: Uint8Array, cell: Cell, ...
* @param indexHashmap cellHash: Uint8Array -> cellIndex: number
* @return {[[], {}]} topologicalOrderArray and indexHashmap
*/
async function treeWalk(cell, topologicalOrderArray, indexHashmap, parentHash = null) {
const cellHash = await cell.hash();
if (cellHash in indexHashmap) { // Duplication cell
//it is possible that already seen cell is a children of more deep cell
if (parentHash) {
if (indexHashmap[parentHash] > indexHashmap[cellHash]) {
await moveToTheEnd(indexHashmap, topologicalOrderArray, cellHash);
}
}
return [topologicalOrderArray, indexHashmap];
}
indexHashmap[cellHash] = topologicalOrderArray.length;
topologicalOrderArray.push([cellHash, cell]);
for (let subCell of cell.refs) {
const res = await treeWalk(subCell, topologicalOrderArray, indexHashmap, cellHash);
topologicalOrderArray = res[0];
indexHashmap = res[1];
}
return [topologicalOrderArray, indexHashmap];
}
function parseBocHeader(serializedBoc) {
// snake_case is used to match TON docs
if (serializedBoc.length < 4 + 1)
throw "Not enough bytes for magic prefix";
const inputData = serializedBoc; // Save copy for crc32
const prefix = serializedBoc.slice(0, 4);
serializedBoc = serializedBoc.slice(4);
let has_idx, hash_crc32, has_cache_bits, flags, size_bytes;
if (compareBytes(prefix, reachBocMagicPrefix)) {
const flags_byte = serializedBoc[0];
has_idx = flags_byte & 128;
hash_crc32 = flags_byte & 64;
has_cache_bits = flags_byte & 32;
flags = (flags_byte & 16) * 2 + (flags_byte & 8);
size_bytes = flags_byte % 8;
}
if (compareBytes(prefix, leanBocMagicPrefix)) {
has_idx = 1;
hash_crc32 = 0;
has_cache_bits = 0;
flags = 0;
size_bytes = serializedBoc[0];
}
if (compareBytes(prefix, leanBocMagicPrefixCRC)) {
has_idx = 1;
hash_crc32 = 1;
has_cache_bits = 0;
flags = 0;
size_bytes = serializedBoc[0];
}
serializedBoc = serializedBoc.slice(1);
if (serializedBoc.length < 1 + 5 * size_bytes)
throw "Not enough bytes for encoding cells counters";
const offset_bytes = serializedBoc[0];
serializedBoc = serializedBoc.slice(1);
const cells_num = readNBytesUIntFromArray(size_bytes, serializedBoc);
serializedBoc = serializedBoc.slice(size_bytes);
const roots_num = readNBytesUIntFromArray(size_bytes, serializedBoc);
serializedBoc = serializedBoc.slice(size_bytes);
const absent_num = readNBytesUIntFromArray(size_bytes, serializedBoc);
serializedBoc = serializedBoc.slice(size_bytes);
const tot_cells_size = readNBytesUIntFromArray(offset_bytes, serializedBoc);
serializedBoc = serializedBoc.slice(offset_bytes);
if (serializedBoc.length < roots_num * size_bytes)
throw "Not enough bytes for encoding root cells hashes";
let root_list = [];
for (let c = 0; c < roots_num; c++) {
root_list.push(readNBytesUIntFromArray(size_bytes, serializedBoc));
serializedBoc = serializedBoc.slice(size_bytes);
}
let index = false;
if (has_idx) {
index = [];
if (serializedBoc.length < offset_bytes * cells_num)
throw "Not enough bytes for index encoding";
for (let c = 0; c < cells_num; c++) {
index.push(readNBytesUIntFromArray(offset_bytes, serializedBoc));
serializedBoc = serializedBoc.slice(offset_bytes);
}
}
if (serializedBoc.length < tot_cells_size)
throw "Not enough bytes for cells data";
const cells_data = serializedBoc.slice(0, tot_cells_size);
serializedBoc = serializedBoc.slice(tot_cells_size);
if (hash_crc32) {
if (serializedBoc.length < 4)
throw "Not enough bytes for crc32c hashsum";
const length = inputData.length;
if (!compareBytes(crc32c(inputData.slice(0, length - 4)), serializedBoc.slice(0, 4)))
throw "Crc32c hashsum mismatch";
serializedBoc = serializedBoc.slice(4);
}
if (serializedBoc.length)
throw "Too much bytes in BoC serialization";
return {
has_idx: has_idx, hash_crc32: hash_crc32, has_cache_bits: has_cache_bits, flags: flags, size_bytes: size_bytes,
off_bytes: offset_bytes, cells_num: cells_num, roots_num: roots_num, absent_num: absent_num,
tot_cells_size: tot_cells_size, root_list: root_list, index: index,
cells_data: cells_data
};
}
function deserializeCellData(cellData, referenceIndexSize) {
if (cellData.length < 2)
throw "Not enough bytes to encode cell descriptors";
const d1 = cellData[0], d2 = cellData[1];
cellData = cellData.slice(2);
const level = Math.floor(d1 / 32);
const isExotic = d1 & 8;
const refNum = d1 % 8;
const dataBytesize = Math.ceil(d2 / 2);
const fullfilledBytes = !(d2 % 2);
let cell = new Cell();
cell.isExotic = isExotic;
if (cellData.length < dataBytesize + referenceIndexSize * refNum)
throw "Not enough bytes to encode cell data";
cell.bits.setTopUppedArray(cellData.slice(0, dataBytesize), fullfilledBytes);
cellData = cellData.slice(dataBytesize);
for (let r = 0; r < refNum; r++) {
cell.refs.push(readNBytesUIntFromArray(referenceIndexSize, cellData));
cellData = cellData.slice(referenceIndexSize);
}
return {cell: cell, residue: cellData};
}
/**
* @param serializedBoc {string | Uint8Array} hex or bytearray
* @return {Cell[]} root cells
*/
function deserializeBoc(serializedBoc) {
if (typeof (serializedBoc) == 'string') {
serializedBoc = hexToBytes(serializedBoc);
}
const header = parseBocHeader(serializedBoc);
let cells_data = header.cells_data;
let cells_array = [];
for (let ci = 0; ci < header.cells_num; ci++) {
let dd = deserializeCellData(cells_data, header.size_bytes);
cells_data = dd.residue;
cells_array.push(dd.cell);
}
for (let ci = header.cells_num - 1; ci >= 0; ci--) {
let c = cells_array[ci];
for (let ri = 0; ri < c.refs.length; ri++) {
const r = c.refs[ri];
if (r < ci) {
throw "Topological order is broken";
}
c.refs[ri] = cells_array[r];
}
}
let root_cells = [];
for (let ri of header.root_list) {
root_cells.push(cells_array[ri]);
}
return root_cells;
}
module.exports = {Cell};