Open newset opened 1 year ago
Hi! Good suggestion — currently Jetton APIs are kinda lame, so everything has to be done "by hand". No worries though, they will become much more handy in the future ✌
I don't use them much, but I'll make sure to compose a comprehensive example here once I get more time.
hi novusnota ,any update on jetticon, I also made some update for ton name service
Hi, not yet. If you have any examples, feel free to share them :) Eventually I'll find time and priority to include them in the repo, but for now it may be enough to link to them from this issue
for ton domain .ton
my implementation
class TonJsonRpc {
parseResponseStack(pair) {
final typeName = pair[0];
final value = pair[1];
switch (typeName) {
case "num":
return BigInt.parse(value);
case "list":
case "tuple":
return parseObject(value);
case "cell":
return Cell.fromBocBase64(value['bytes']);
default:
throw Exception("unknown type $typeName");
}
}
Future categoryToBN(String category) async {
if (category.isEmpty) return BigInt.from(0);
final categoryBytes = utf8.encode(category);
Uint8List categoryHash =
SHA256Digest().process(Uint8List.fromList(categoryBytes));
final result = BigInt.parse(hex.encode(categoryHash), radix: 16);
return result;
}
Future resolveDomain(String domainName) async {
final rootDnsAddress = await getRootDnsAddress();
Uint8List bytes = domainToBytes(domainName);
final resolve =
await dnsResolveImpl(bytes, rootDnsAddress.toRawString(), 'wallet');
return resolve;
}
Future<List<dynamic>> call2(address, method, List params) async {
var rawResult = await _call(
'runGetMethod',
<String, dynamic>{"address": address, "method": method, "stack": params},
);
if (rawResult['exit_code'] != 0) {
throw Exception("http provider parse response error");
}
List<dynamic> stack = rawResult['stack'];
final arr = stack.map(parseResponseStack).toList();
return arr.length == 1 ? arr[0] : arr;
}
Uint8List domainToBytes(String domain) {
if (domain.isEmpty) {
throw ArgumentError("empty domain");
}
if (domain == ".") {
return Uint8List.fromList([0]);
}
domain = domain.toLowerCase();
for (int i = 0; i < domain.length; i++) {
if (domain.codeUnitAt(i) <= 32) {
throw ArgumentError(
"bytes in range 0..32 are not allowed in domain names");
}
}
for (int i = 0; i < domain.length; i++) {
String s = domain.substring(i, i + 1);
for (int c = 127; c <= 159; c++) {
// another control codes range
if (s == String.fromCharCode(c)) {
throw ArgumentError(
"bytes in range 127..159 are not allowed in domain names");
}
}
}
List<String> arr = domain.split(".");
for (var part in arr) {
if (part.isEmpty) {
throw ArgumentError("domain name cannot have an empty component");
}
}
String rawDomain = "${arr.reversed.join("\x00")}${"\x00"}";
if (rawDomain.length < 126) {
rawDomain = "${"\x00"}$rawDomain";
}
return Uint8List.fromList(utf8.encode(rawDomain));
}
Future<InternalAddress> getRootDnsAddress() async {
final cell = await _getConfigParam(4);
final byteArray = cell.bits.toPaddedList();
if (byteArray.length != 256 / 8) {
throw Exception("Invalid ConfigParam 4 length ${byteArray.length}");
}
final hexMatch = hex.encode(byteArray);
return InternalAddress.parseRaw("-1:$hexMatch");
}
Future<Cell> _getConfigParam(
int configParamId,
) async {
var rawResult = await _call(
'getConfigParam',
<String, dynamic>{"config_id": configParamId},
);
if (rawResult['@type'] != 'configInfo') {
throw Exception('getConfigParam expected type configInfo');
}
if (rawResult['config'] == null) {
throw Exception('getConfigParam expected config');
}
if (rawResult['config']['@type'] != 'tvm.cell') {
throw Exception('getConfigParam expected type tvm.cell');
}
if (rawResult['config']['bytes'] == null) {
throw Exception('getConfigParam expected bytes');
}
return Cell.fromBocBase64(rawResult['config']['bytes']);
}
BigInt readIntFromBitString(List bs, int cursor, int bits) {
BigInt n = BigInt.from(0);
for (int i = 0; i < bits; i++) {
n *= BigInt.from(2);
n += BigInt.from(_get(bs, cursor + i));
}
return n;
}
int _get(List data, int index) {
final get = (data[index ~/ 8 | 0] & (1 << (7 - (index % 8)))) > 0;
return get ? 1 : 0;
}
InternalAddress parseAddress(List bits) {
BigInt n = readIntFromBitString(bits, 3, 8);
if (n > BigInt.from(127)) {
n = n - BigInt.from(256);
}
final hashPart = readIntFromBitString(bits, 3 + 8, 256);
if ("$n:$hashPart" == "0:0") {
throw Exception('not found');
}
final s =
"${n.toRadixString(10)}:${hashPart.toRadixString(16).padRight(64, "0")}";
return InternalAddress.parseRaw(s);
}
InternalAddress parseSmartContractAddressImpl(Cell cell, prefix0, prefix1) {
List<int> bits = cell.bits.toPaddedList();
if (bits[0] != prefix0 || bits[1] != prefix1) {
throw Exception("Invalid dns record value prefix");
}
bits[bits.length - 1] = bits[bits.length - 1] - 16;
bits = bits.slice(2);
return parseAddress(bits);
}
InternalAddress parseSmartContractAddressRecord(Cell cell) {
return parseSmartContractAddressImpl(cell, 0x9f, 0xd3);
}
InternalAddress parseNextResolverRecord(Cell cell) {
return parseSmartContractAddressImpl(cell, 0xba, 0x93);
}
Future<InternalAddress?> dnsResolveImpl(
Uint8List rawDomainBytes,
String dnsAddress,
String category,
) async {
final len = rawDomainBytes.length * 8;
var builder = BitBuilder();
builder.writeList(rawDomainBytes);
final domainCell = Cell(bits: BitString(builder.list(), 0, builder.length));
final categoryBN = await categoryToBN('wallet');
final result = await call2(dnsAddress, "dnsresolve", [
["tvm.Slice", base64.encode(domainCell.toBoc())],
["num", categoryBN.toString()],
]);
BigInt first = result[0];
int resultLen = first.toInt();
Cell cell = result[1];
if (resultLen == 0) {
return null;
}
if (resultLen % 8 != 0) {
throw Exception("domain split not at a component boundary");
}
const DNS_CATEGORY_NEXT_RESOLVER = "dns_next_resolver";
const DNS_CATEGORY_WALLET = "wallet";
const DNS_CATEGORY_SITE = "site";
const DNS_CATEGORY_STORAGE = "storage";
if (resultLen > len) {
throw Exception("invalid response $resultLen/$len");
} else if (resultLen == len) {
if (category == DNS_CATEGORY_NEXT_RESOLVER) {
return cell != Cell.empty ? parseNextResolverRecord(cell) : null;
} else if (category == DNS_CATEGORY_WALLET) {
return cell != Cell.empty
? parseSmartContractAddressRecord(cell)
: null;
} else {
throw Exception('not supported');
}
// else if (category == DNS_CATEGORY_SITE) {
// return cell != Cell.empty ? parseSiteRecord(cell) : null;
// } else if (category == DNS_CATEGORY_STORAGE) {
// return cell != Cell.empty ? parseStorageBagIdRecord(cell) : null;
// } else {
// return cell;
// }
} else {
if (cell == Cell.empty) {
return null; // domain cannot be resolved
} else {
const oneStep = false;
final nextAddress = parseNextResolverRecord(cell);
if (oneStep) {
if (category == DNS_CATEGORY_NEXT_RESOLVER) {
return nextAddress;
} else {
return null;
}
} else {
final newBytes = rawDomainBytes.slice(resultLen ~/ 8).toList();
return await dnsResolveImpl(
Uint8List.fromList(newBytes),
nextAddress.toRawString(),
category,
);
}
}
}
}
}
final address = await ton.TonJsonRpc(
api,
tonApiKey,
).resolveDomain(domainName);
Thanks!
tried to figure out the way to transfer jetton, failed. need help here. really appreciate your work, this package is great.