novusnota / tonutils-dart

Comprehensive Dart SDK for interacting with TON Blockchain. When combined with Flutter can be used for any popular platform out there!
https://pub.dev/packages/tonutils
Apache License 2.0
19 stars 2 forks source link

Any example on transfer jetton? #3

Open newset opened 1 year ago

newset commented 1 year ago

tried to figure out the way to transfer jetton, failed. need help here. really appreciate your work, this package is great.

novusnota commented 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.

Imdavyking commented 3 months ago

hi novusnota ,any update on jetticon, I also made some update for ton name service

novusnota commented 3 months ago

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

Imdavyking commented 3 months ago

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,
          );
        }
      }
    }
  }

}
Imdavyking commented 3 months ago
final address = await ton.TonJsonRpc(
      api,
      tonApiKey,
    ).resolveDomain(domainName);
novusnota commented 3 months ago

Thanks!