@ton3/liteclient
Version:
TON Blockchain LiteClient
544 lines (482 loc) • 16.8 kB
text/typescript
import {
Account,
AccountStatus,
AccountTransactionId,
BlockId,
Transaction,
TransactionId,
} from '@tonkite/core';
import { Address, BOC, Coins, Hashmap, Slice, Utils, Bit } from 'ton3-core';
import { LiteApi, WaitOptions } from '../liteapi';
import { AccountId, RunMethodResult } from '../../dataTypes/liteServer';
import { Stack, StackValue } from '../../utils/stack'
import {
loadAccount,
loadCurrencyCollection,
loadShardAccount,
loadShardHashes,
ShardAccount,
} from '@tonkite/types';
import { TransactionCursor } from './cursor';
type Modify<T, R> = Omit<T, keyof R> & R;
type GetMethodResponse<T> = Modify<RunMethodResult, { result: T | null }>
export class BlockchainClient {
constructor(private readonly api: LiteApi) {}
async getMasterchainInfo(wait?: WaitOptions): Promise<BlockId> {
const masterchainInfo = await this.api.getMasterchainInfo(wait);
return {
workchain: masterchainInfo.last.workchain,
shard: masterchainInfo.last.shard,
seqno: masterchainInfo.last.seqno,
rootHash: masterchainInfo.last.root_hash,
fileHash: masterchainInfo.last.file_hash,
};
}
async getAllShardsInfo(blockId: BlockId, wait?: WaitOptions): Promise<BlockId[]> {
const allShards = await this.api.getAllShardsInfo({
workchain: blockId.workchain,
shard: blockId.shard,
seqno: blockId.seqno,
root_hash: blockId.rootHash,
file_hash: blockId.fileHash,
}, wait);
const [ cell ] = BOC.from(allShards.data)
const shardHashes = loadShardHashes(cell.slice());
return shardHashes
.map((workchain) =>
workchain.shards.map(
(shard): BlockId => ({
workchain: workchain.workchain,
shard: shard.id,
seqno: shard.description.seq_no,
rootHash: shard.description.root_hash,
fileHash: shard.description.file_hash,
}),
),
)
.flat();
}
async lookupBlock(blockId: Pick<BlockId, 'workchain' | 'shard' | 'seqno'>, wait?: WaitOptions): Promise<BlockId> {
const result = await this.api.lookupBlock({
id: {
workchain: blockId.workchain,
shard: blockId.shard,
seqno: blockId.seqno,
},
}, wait);
return {
workchain: result.id.workchain,
shard: result.id.shard,
seqno: result.id.seqno,
rootHash: result.id.root_hash,
fileHash: result.id.file_hash,
};
}
async getAccountState(
data: {
account: Address,
blockId: BlockId
},
wait?: WaitOptions
): Promise<Account> {
const accountStateRaw = await this.api.getAccountState({
blockId: {
workchain: data.blockId.workchain,
shard: data.blockId.shard,
seqno: data.blockId.seqno,
root_hash: data.blockId.rootHash,
file_hash: data.blockId.fileHash,
},
account: {
workchain: data.account.workchain,
id: data.account.hash,
}
}, wait);
if (!accountStateRaw.state.length) {
return new Account(
{
workchain: accountStateRaw.id.workchain,
shard: accountStateRaw.id.shard,
seqno: accountStateRaw.id.seqno,
rootHash: accountStateRaw.id.root_hash,
fileHash: accountStateRaw.id.file_hash,
},
data.account,
Coins.fromNano(0),
{
lt: 0n,
hash: new Uint8Array(32),
},
AccountStatus.UNINITIALIZED,
null,
null,
null,
{
lastPaid: null,
duePayment: null,
usage: {
bits: 0,
cells: 0,
publicCells: 0,
},
},
);
}
const [ _, proof ] = BOC.from(accountStateRaw.proof);
/**
* shard_ident$00 shard_pfx_bits:(#<= 60)
* workchain_id:int32 shard_prefix:uint64 = ShardIdent;
*/
const loadShardIdent = (slice: Slice) => {
if (slice.loadUint(2) !== 0b00) {
throw new Error('Incorrect ShardIdent');
}
const shardPrefixBits = slice.loadBigUint(6); // NOTE: log2(60 + 1) = 5.93
const workchainId = slice.loadInt(32);
const shardPrefix = slice.loadBigUint(64);
return {
shardPrefixBits,
workchainId,
shardPrefix,
};
};
const loadDepthBalanceInfo = (slice: Slice) => {
const splitDepth = slice.loadUint(5); // log2(30 + 1) = 4.95
const balance = loadCurrencyCollection(slice);
return {
splitDepth,
balance,
};
};
/**
* depth_balance$_ split_depth:(#<= 30) balance:CurrencyCollection = DepthBalanceInfo;
* _ (HashmapAugE 256 ShardAccount DepthBalanceInfo) = ShardAccounts;
*/
const loadShardAccounts = (slice: Slice): [Uint8Array, { shardAccount: ShardAccount }][] => {
return [
...Hashmap.parse<Uint8Array, { shardAccount: ShardAccount }>(
256,
Slice.parse(slice.refs[0]),
{
deserializers: {
key: Utils.Helpers.bitsToBytes,
value: (cell) => {
const cs = Slice.parse(cell);
const depthBalanceInfo = loadDepthBalanceInfo(cs);
const shardAccount = loadShardAccount(cs);
return {
shardAccount,
depthBalanceInfo,
};
},
},
},
),
];
};
/**
* shard_state#9023afe2 global_id:int32
* shard_id:ShardIdent
* seq_no:uint32 vert_seq_no:#
* gen_utime:uint32 gen_lt:uint64
* min_ref_mc_seqno:uint32
* out_msg_queue_info:^OutMsgQueueInfo
* before_split:(## 1)
* accounts:^ShardAccounts
* ^[ overload_history:uint64 underload_history:uint64
* total_balance:CurrencyCollection
* total_validator_fees:CurrencyCollection
* libraries:(HashmapE 256 LibDescr)
* master_ref:(Maybe BlkMasterInfo) ]
* custom:(Maybe ^McStateExtra)
* = ShardStateUnsplit;
*/
const loadShardStateUnsplit = (slice: Slice) => {
if (slice.loadUint(32) !== 0x9023afe2) {
throw new Error('Incorrect ShardStateUnsplit');
}
const global_id = slice.loadInt(32);
const shard_id = loadShardIdent(slice);
const seq_no = slice.loadUint(32);
const vert_seq_no = slice.loadUint(32);
const gen_utime = slice.loadUint(32);
const gen_lt = slice.loadBigUint(64);
const min_ref_mc_seqno = slice.loadUint(32);
const out_msg_queue_info = slice.loadRef(); // ^OutMsgQueueInfo
const before_split = slice.loadBit();
const shardAccounts = loadShardAccounts(Slice.parse(slice.loadRef()));
const next = Slice.parse(slice.loadRef());
const overload_history = next.loadBigUint(64);
const underload_history = next.loadBigUint(64);
// const total_balance = loadCurrencyCollection(next);
// const total_validator_fees = loadCurrencyCollection(next);
return {
global_id,
shard_id,
seq_no,
vert_seq_no,
gen_utime,
gen_lt,
min_ref_mc_seqno,
out_msg_queue_info,
before_split,
shardAccounts,
overload_history,
underload_history,
// total_balance,
// total_validator_fees,
};
};
const [ cell ] = BOC.from(accountStateRaw.state)
const shardState = loadShardStateUnsplit(proof.refs[0].slice());
const accountState = loadAccount(cell.slice());
const shardAccount = shardState.shardAccounts
.filter(([accountHash]) => {
return Buffer.from(accountHash).equals(Buffer.from(data.account.hash, 0));
})
.map(([_, { shardAccount }]) => shardAccount)[0];
return new Account(
{
workchain: accountStateRaw.id.workchain,
shard: accountStateRaw.id.shard,
seqno: accountStateRaw.id.seqno,
rootHash: accountStateRaw.id.root_hash,
fileHash: accountStateRaw.id.file_hash,
},
data.account,
accountState.storage.balance.coins,
{
lt: shardAccount.lastTransactionId.lt,
hash: shardAccount.lastTransactionId.hash,
},
accountState.storage.state.type as AccountStatus,
accountState.storage.state.type === 'frozen' ? accountState.storage.state.stateHash : null,
accountState.storage.state.type === 'active' ? accountState.storage.state.code : null,
accountState.storage.state.type === 'active' ? accountState.storage.state.data : null,
{
lastPaid: new Date(accountState.storageStat.lastPaid * 1000),
duePayment: accountState.storageStat.duePayment,
usage: {
bits: accountState.storageStat.used.bits,
cells: accountState.storageStat.used.cells,
publicCells: accountState.storageStat.used.publicCells,
},
},
);
}
private shardIdent (slice: Slice) {
slice.skipBits(2)
const shard_pfx_bits = slice.loadBits(Math.ceil(Math.log2(60 + 1)))
const workchain_id = slice.loadInt(32)
const shard_prefix = slice.loadBigUint(64)
return {
shard_pfx_bits,
workchain_id,
shard_prefix
}
}
private globalVersion (slice: Slice) {
slice.skipBits(8)
const version = slice.loadUint(32)
const capabilities = slice.loadBigUint(64)
return {
version,
capabilities
}
}
private extBlkRef (slice: Slice) {
const end_lt = slice.loadBigUint(64)
const seq_no = slice.loadUint(32)
const root_hash = slice.loadBytes(256)
const file_hash = slice.loadBytes(256)
return {
end_lt,
seq_no,
root_hash,
file_hash
}
}
private blkMasterInfo (slice: Slice) {
const master = this.extBlkRef(slice)
return {
master
}
}
private blkPrevInfo (slice: Slice, arg: Bit) {
if (arg === 0) {
return this.extBlkRef(slice)
}
return [
this.extBlkRef(slice.loadRef().slice()),
this.extBlkRef(slice.loadRef().slice())
]
}
private blkInfo (slice: Slice) {
slice.skipBits(32)
const version = slice.loadUint(32)
const not_master = slice.loadBit()
const after_merge = slice.loadBit()
const before_split = slice.loadBit()
const after_split = slice.loadBit()
const want_split = Boolean(slice.loadBit())
const want_merge = Boolean(slice.loadBit())
const key_block = Boolean(slice.loadBit())
const vert_seqno_incr = slice.loadBit()
const flags = slice.loadUint(8)
const seq_no = slice.loadUint(32)
const vert_seq_no = slice.loadUint(32)
const shard = this.shardIdent(slice)
const gen_utime = slice.loadUint(32)
const start_lt = slice.loadBigUint(64)
const end_lt = slice.loadBigUint(64)
const gen_validator_list_hash_short = slice.loadUint(32)
const gen_catchain_seqno = slice.loadUint(32)
const min_ref_mc_seqno = slice.loadUint(32)
const prev_key_block_seqno = slice.loadUint(32)
const gen_software = (flags & (1 << 0)) ? this.globalVersion(slice) : null
const master_ref = Boolean(not_master) ? this.blkMasterInfo(slice.loadRef().slice()) : null
const prev_ref = this.blkPrevInfo(slice.loadRef().slice(), after_merge)
const prev_vert_ref = Boolean(vert_seqno_incr) ? this.blkPrevInfo(slice.loadRef().slice(), 0) : null
return {
version,
not_master,
after_merge,
before_split,
after_split,
want_split,
want_merge,
key_block,
vert_seqno_incr,
flags,
seq_no,
vert_seq_no,
shard,
gen_utime,
start_lt,
end_lt,
gen_validator_list_hash_short,
gen_catchain_seqno,
min_ref_mc_seqno,
prev_key_block_seqno,
gen_software,
master_ref,
prev_ref,
prev_vert_ref
}
}
public async getBlock (blockId: BlockId, wait?: WaitOptions) {
const { data } = await this.api.getBlock({
workchain: blockId.workchain,
shard: blockId.shard,
seqno: blockId.seqno,
root_hash: blockId.rootHash,
file_hash: blockId.fileHash
}, wait)
const slice = BOC.from(data).root[0].slice()
slice.skipBits(32)
const global_id = slice.loadInt(32)
const info = this.blkInfo(slice.loadRef().slice())
const value_flow = slice.loadRef()
const state_update = slice.loadRef()
const extra = slice.loadRef()
return {
global_id,
info,
value_flow,
state_update,
extra
}
}
async getBlockTransactions(
data: {
blockId: BlockId,
after?: Pick<TransactionId, 'account' | 'lt'> | null,
},
wait?: WaitOptions
): Promise<TransactionId[]> {
const result = await this.api.listBlockTransactions({
blockId: {
workchain: data.blockId.workchain,
shard: data.blockId.shard,
seqno: data.blockId.seqno,
root_hash: data.blockId.rootHash,
file_hash: data.blockId.fileHash,
},
count: 40,
after: data.after ? { account: data.after.account.hash, lt: data.after.lt } : null
}, wait);
return result.ids.map((id) => ({
account: new Address(`${result.id.workchain}:${Utils.Helpers.bytesToHex(id.account!)}`),
lt: id.lt!,
hash: id.hash!,
}));
}
async getOneTransaction(
data: {
blockId: BlockId,
account: AccountId,
lt: bigint
},
wait?: WaitOptions
) {
return await this.api.getOneTransaction({
blockId: {
workchain: data.blockId.workchain,
shard: data.blockId.shard,
seqno: data.blockId.seqno,
root_hash: data.blockId.rootHash,
file_hash: data.blockId.fileHash
},
accountId: data.account,
lt: data.lt
}, wait)
}
getTransactions(
data: {
account: Address,
after: AccountTransactionId,
take?: number
}
): AsyncIterable<Transaction> {
const { account, after, take = Infinity } = data
return new TransactionCursor(this.api, account, after, take);
}
public async invokeGetMethod<T extends StackValue[] = StackValue[]> (
data: {
account: Account,
method: string | number,
stack: StackValue[]
},
wait?: WaitOptions
): Promise<GetMethodResponse<T>> {
const { account, method, stack = [] } = data
const methodName = typeof method !== 'number' ? Utils.Helpers.stringToBytes(method) : null
const methodId = BigInt(typeof method !== 'number' ? (Utils.Checksum.crc16(methodName) & 0xffff) | 0x10000 : method)
const blockId = {
workchain: account.blockId.workchain,
shard: account.blockId.shard,
seqno: account.blockId.seqno,
root_hash: account.blockId.rootHash,
file_hash: account.blockId.fileHash
}
const accountId = {
workchain: account.address.workchain,
id: account.address.hash
}
const params = new BOC([ Stack.serialize(stack) ]).toBytes()
const response = await this.api.runSmcMethod({
blockId,
accountId,
methodId,
params
}, wait)
const result: GetMethodResponse<T> = {
...response,
result: response.result ? Stack.deserialize<T>(BOC.from(response.result).root[0]) : null
}
return result
}
public async getTime(wait?: WaitOptions) {
return await this.api.getTime(wait)
}
}