@openproduct/web-sdk
Version:
OpenMask Web - TypeScript Browser SDK for TON blockchain
258 lines (232 loc) • 7.34 kB
text/typescript
import BN from "bn.js";
import { Cell } from "../../boc/cell";
import TonHttpProvider from "../../providers/httpProvider";
import Address from "../../utils/address";
import { bytesToBase64, bytesToHex, sha256_sync } from "../../utils/utils";
import { parseAddress } from "../token/nft/utils";
export const DNS_CATEGORY_NEXT_RESOLVER = "dns_next_resolver"; // Smart Contract address
export const DNS_CATEGORY_WALLET = "wallet"; // Smart Contract address
export const DNS_CATEGORY_SITE = "site"; // ADNL address
/**
* @param category {string | undefined}
* @return {BN}
*/
export const categoryToBN = (category: string | undefined) => {
if (!category) return new BN(0); // all categories
const categoryBytes = new TextEncoder().encode(category);
const categoryHash = new Uint8Array(sha256_sync(categoryBytes));
return new BN(bytesToHex(categoryHash), 16);
};
/**
* @param smartContractAddress {Address}
* @return {Cell}
*/
export const createSmartContractAddressRecord = (
smartContractAddress: Address
) => {
const cell = new Cell();
cell.bits.writeUint(0x9fd3, 16); // https://github.com/ton-blockchain/ton/blob/7e3df93ca2ab336716a230fceb1726d81bac0a06/crypto/block/block.tlb#L827
cell.bits.writeAddress(smartContractAddress);
cell.bits.writeUint(0, 8); // flags
return cell;
};
/**
* @param adnlAddress {BN}
* @return {Cell}
*/
export const createAdnlAddressRecord = (adnlAddress: BN) => {
const cell = new Cell();
cell.bits.writeUint(0xad01, 16); // https://github.com/ton-blockchain/ton/blob/7e3df93ca2ab336716a230fceb1726d81bac0a06/crypto/block/block.tlb#L821
cell.bits.writeUint(adnlAddress, 256);
cell.bits.writeUint(0, 8); // flags
return cell;
};
/**
* @param smartContractAddress {Address}
* @return {Cell}
*/
export const createNextResolverRecord = (smartContractAddress: Address) => {
const cell = new Cell();
cell.bits.writeUint(0xba93, 16); // https://github.com/ton-blockchain/ton/blob/7e3df93ca2ab336716a230fceb1726d81bac0a06/crypto/block/block.tlb#L819
cell.bits.writeAddress(smartContractAddress);
return cell;
};
/**
* @private
* @param cell {Cell}
* @param prefix0 {number}
* @param prefix1 {number}
* @return {Address|null}
*/
const parseSmartContractAddressImpl = (
cell: Cell,
prefix0: number,
prefix1: number
) => {
if (cell.bits.array[0] !== prefix0 || cell.bits.array[1] !== prefix1)
throw new Error("Invalid dns record value prefix");
cell.bits.array = cell.bits.array.slice(2); // skip prefix - first 16 bits
return parseAddress(cell);
};
/**
* @param cell {Cell}
* @return {Address|null}
*/
export const parseSmartContractAddressRecord = (cell: Cell) => {
return parseSmartContractAddressImpl(cell, 0x9f, 0xd3);
};
/**
* @param cell {Cell}
* @return {Address|null}
*/
export const parseNextResolverRecord = (cell: Cell) => {
return parseSmartContractAddressImpl(cell, 0xba, 0x93);
};
/**
* @private
* @param provider {TonHttpProvider}
* @param dnsAddress {string} address of dns smart contract
* @param rawDomainBytes {Uint8Array}
* @param category {string | undefined} category of requested DNS record
* @param oneStep {boolean | undefined} non-recursive
* @returns {Promise<Cell | Address | BN | null>}
*/
const dnsResolveImpl = async (
provider: TonHttpProvider,
dnsAddress: string,
rawDomainBytes: Uint8Array,
category: string | undefined,
oneStep: boolean | undefined
): Promise<Cell | Address | BN | null> => {
const len = rawDomainBytes.length * 8;
const domainCell = new Cell();
domainCell.bits.writeBytes(rawDomainBytes);
const categoryBN = await categoryToBN(category);
const result = await provider.call2(dnsAddress, "dnsresolve", [
["tvm.Slice", bytesToBase64(await domainCell.toBoc(false))],
["num", categoryBN.toString()],
]);
if (result.length !== 2) {
throw new Error("Invalid dnsresolve response");
}
const resultLen = result[0].toNumber();
let cell = result[1];
if (cell instanceof Array && cell.length === 0) {
cell = null;
}
if (cell && !cell.bits) {
// not a Cell
throw new Error("Invalid dnsresolve response");
}
if (resultLen === 0) {
return null; // domain cannot be resolved
}
if (resultLen % 8 !== 0) {
throw new Error("domain split not at a component boundary");
}
// if (rawDomainBytes[resultLen] !== 0) {
// throw new Error('domain split not at a component boundary');
// }
if (resultLen > len) {
throw new Error("invalid response " + resultLen + "/" + len);
} else if (resultLen === len) {
if (category === DNS_CATEGORY_NEXT_RESOLVER) {
return cell ? parseNextResolverRecord(cell) : null;
} else if (category === DNS_CATEGORY_WALLET) {
return cell ? parseSmartContractAddressRecord(cell) : null;
} else if (category === DNS_CATEGORY_SITE) {
return cell ? cell : null; // todo: convert to BN;
} else {
return cell;
}
} else {
if (!cell) {
return null; // domain cannot be resolved
} else {
const nextAddress = parseNextResolverRecord(cell);
if (oneStep) {
if (category === DNS_CATEGORY_NEXT_RESOLVER) {
return nextAddress;
} else {
return null;
}
} else {
if (!nextAddress) {
return null;
}
return await dnsResolveImpl(
provider,
nextAddress.toString(),
rawDomainBytes.slice(resultLen / 8),
category,
false
);
}
}
}
};
/**
* Verify and convert domain
* @param domain {string}
* @return {Uint8Array}
*/
const domainToBytes = (domain: string) => {
if (!domain || !domain.length) {
throw new Error("empty domain");
}
if (domain === ".") {
return new Uint8Array([0]);
}
domain = domain.toLowerCase();
for (let i = 0; i < domain.length; i++) {
if (domain.charCodeAt(i) <= 32) {
throw new Error("bytes in range 0..32 are not allowed in domain names");
}
}
for (let i = 0; i < domain.length; i++) {
const s = domain.substring(i, i + 1);
for (let c = 127; c <= 159; c++) {
// another control codes range
if (s === String.fromCharCode(c)) {
throw new Error(
"bytes in range 127..159 are not allowed in domain names"
);
}
}
}
const arr = domain.split(".");
arr.forEach((part) => {
if (!part.length) {
throw new Error("domain name cannot have an empty component");
}
});
let rawDomain = arr.reverse().join("\0") + "\0";
if (rawDomain.length < 126) {
rawDomain = "\0" + rawDomain;
}
return new TextEncoder().encode(rawDomain);
};
/**
* @param provider {TonHttpProvider}
* @param rootDnsAddress {string} address of root DNS smart contract
* @param domain {string} e.g "sub.alice.ton"
* @param category {string | undefined} category of requested DNS record
* @param oneStep {boolean | undefined} non-recursive
* @returns {Promise<Cell | Address | BN | null>}
*/
export const dnsResolve = async (
provider: TonHttpProvider,
rootDnsAddress: string,
domain: string,
category: string | undefined,
oneStep: boolean | undefined
) => {
const rawDomainBytes = domainToBytes(domain);
return dnsResolveImpl(
provider,
rootDnsAddress,
rawDomainBytes,
category,
oneStep
);
};