@openproduct/web-sdk
Version:
OpenMask Web - TypeScript Browser SDK for TON blockchain
301 lines (280 loc) • 8.53 kB
text/typescript
import BN from "bn.js";
import { Cell } from "../boc/cell";
import TonHttpProvider, { EstimateFee } from "../providers/httpProvider";
import Address from "../utils/address";
import { bytesToBase64, bytesToHex } from "../utils/utils";
export interface ExternalMessage {
address: Address;
message: Cell;
body: Cell;
signature?: Uint8Array;
signingMessage?: Cell;
stateInit: Cell | null;
code: Cell | null;
data: Cell | null;
}
export type Options = {
code?: Cell;
data?: Cell;
address?: Address | string;
wc?: number;
[key: string]: any;
};
export interface Method {
getQuery: () => Promise<Cell>;
send: () => Promise<void>;
estimateFee: () => Promise<EstimateFee>;
}
export class Contract {
provider: TonHttpProvider;
options: Options;
address: Address | null;
methods: Record<string, any>;
/**
* @param provider {TonHttpProvider}
* @param options {{code?: Cell, address?: Address | string, wc?: number}}
*/
constructor(provider: TonHttpProvider, options: Options) {
this.provider = provider;
this.options = options;
this.address = options.address ? new Address(options.address) : null;
if (!options.wc) options.wc = this.address ? this.address.wc : 0;
this.methods = {};
}
/**
* @return {Promise<Address>}
*/
public async getAddress() {
if (!this.address) {
this.address = (await this.createStateInit()).address;
}
return this.address;
}
/**
* @private
* @return {Cell} cell contains contact code
*/
protected createCodeCell() {
if (!this.options.code)
throw new Error("Contract: options.code is not defined");
return this.options.code;
}
/**
* Method to override
* @protected
* @return {Cell} cell contains contract data
*/
protected createDataCell() {
if (!this.options.data) {
return new Cell();
}
return this.options.data;
}
/**
* @protected
* @return {Promise<{stateInit: Cell, address: Address, code: Cell, data: Cell}>}
*/
public async createStateInit() {
const codeCell = this.createCodeCell();
const dataCell = this.createDataCell();
const stateInit = Contract.createStateInit(codeCell, dataCell);
const stateInitHash = await stateInit.hash();
const address = new Address(
this.options.wc + ":" + bytesToHex(stateInitHash)
);
return {
stateInit: stateInit,
address: address,
code: codeCell,
data: dataCell,
};
}
// _ split_depth:(Maybe (## 5)) special:(Maybe TickTock)
// code:(Maybe ^Cell) data:(Maybe ^Cell)
// library:(Maybe ^Cell) = StateInit;
/**
* @param code {Cell}
* @param data {Cell}
* @param library {null}
* @param splitDepth {null}
* @param ticktock {null}
* @return {Cell}
*/
static createStateInit(
code: Uint8Array | Cell,
data: Cell,
library = null,
splitDepth = null,
ticktock = null
) {
if (library) throw "Library in state init is not implemented";
if (splitDepth) throw "Split depth in state init is not implemented";
if (ticktock) throw "Ticktock in state init is not implemented";
const stateInit = new Cell();
stateInit.bits.writeBitArray([
Boolean(splitDepth),
Boolean(ticktock),
Boolean(code),
Boolean(data),
Boolean(library),
]);
if (code) stateInit.refs.push(code);
if (data) stateInit.refs.push(data);
if (library) stateInit.refs.push(library);
return stateInit;
}
// extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32))
// = ExtraCurrencyCollection;
// currencies$_ grams:Grams other:ExtraCurrencyCollection
// = CurrencyCollection;
//int_msg_info$0 ihr_disabled:Bool bounce:Bool
//src:MsgAddressInt dest:MsgAddressInt
//value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
//created_lt:uint64 created_at:uint32 = CommonMsgInfo;
/**
* @param dest {Address | string}
* @param gramValue {number | BN}
* @param ihrDisabled {boolean}
* @param bounce {null | boolean}
* @param bounced {boolean}
* @param src {Address | string}
* @param currencyCollection {null}
* @param ihrFees {number | BN}
* @param fwdFees {number | BN}
* @param createdLt {number | BN}
* @param createdAt {number | BN}
* @return {Cell}
*/
static createInternalMessageHeader(
dest: Address | string,
gramValue: number | BN = 0,
ihrDisabled: boolean = true,
bounce: null | boolean = null,
bounced: boolean = false,
src: Address | string | null = null,
currencyCollection = null,
ihrFees: number | BN = 0,
fwdFees: number | BN = 0,
createdLt: number | BN = 0,
createdAt: number | BN = 0
) {
const message = new Cell();
message.bits.writeBit(false);
message.bits.writeBit(ihrDisabled);
if (!(bounce === null)) {
message.bits.writeBit(bounce);
} else {
message.bits.writeBit(new Address(dest).isBounceable);
}
message.bits.writeBit(bounced);
message.bits.writeAddress(src ? new Address(src) : null);
message.bits.writeAddress(new Address(dest));
message.bits.writeGrams(gramValue);
if (currencyCollection) {
throw "Currency collections are not implemented yet";
}
message.bits.writeBit(Boolean(currencyCollection));
message.bits.writeGrams(ihrFees);
message.bits.writeGrams(fwdFees);
message.bits.writeUint(createdLt, 64);
message.bits.writeUint(createdAt, 32);
return message;
}
//ext_in_msg_info$10 src:MsgAddressExt dest:MsgAddressInt
//import_fee:Grams = CommonMsgInfo;
/**
* @param dest {Address | string}
* @param src {Address | string}
* @param importFee {number | BN}
* @return {Cell}
*/
static createExternalMessageHeader(
dest: Address | string,
src: Address | string | null = null,
importFee = 0
) {
const message = new Cell();
message.bits.writeUint(2, 2);
message.bits.writeAddress(src ? new Address(src) : null);
message.bits.writeAddress(new Address(dest));
message.bits.writeGrams(importFee);
return message;
}
//tblkch.pdf, page 57
/**
* Create CommonMsgInfo contains header, stateInit, body
* @param header {Cell}
* @param stateInit? {Cell}
* @param body? {Cell}
* @return {Cell}
*/
static createCommonMsgInfo(
header: Cell,
stateInit: Cell | null = null,
body: Cell | null = null
) {
const commonMsgInfo = new Cell();
commonMsgInfo.writeCell(header);
if (stateInit) {
commonMsgInfo.bits.writeBit(true);
//-1: need at least one bit for body
// TODO we also should check for free refs here
if (
commonMsgInfo.bits.getFreeBits() - 1 >=
stateInit.bits.getUsedBits()
) {
commonMsgInfo.bits.writeBit(false);
commonMsgInfo.writeCell(stateInit);
} else {
commonMsgInfo.bits.writeBit(true);
commonMsgInfo.refs.push(stateInit);
}
} else {
commonMsgInfo.bits.writeBit(false);
}
// TODO we also should check for free refs here
if (body) {
if (commonMsgInfo.bits.getFreeBits() >= body.bits.getUsedBits()) {
commonMsgInfo.bits.writeBit(false);
commonMsgInfo.writeCell(body);
} else {
commonMsgInfo.bits.writeBit(true);
commonMsgInfo.refs.push(body);
}
} else {
commonMsgInfo.bits.writeBit(false);
}
return commonMsgInfo;
}
static createMethod(
provider: TonHttpProvider,
queryPromise: Promise<ExternalMessage>
): Method {
return {
getQuery: async () => {
return (await queryPromise).message;
},
send: async () => {
const query = await queryPromise;
const boc = bytesToBase64(await query.message.toBoc(false));
return provider.sendBoc(boc);
},
estimateFee: async () => {
const query = await queryPromise;
const serialized =
query.code && query.data // deploy
? {
address: query.address.toString(true, true, false),
body: bytesToBase64(await query.body.toBoc(false)),
init_code: bytesToBase64(await query.code.toBoc(false)),
init_data: bytesToBase64(await query.data.toBoc(false)),
}
: {
address: query.address.toString(true, true, true),
body: bytesToBase64(await query.body.toBoc(false)),
};
return provider.getEstimateFee(serialized);
},
};
}
}