Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example on how to deserialize to TEP64 dictionary. #6

Closed
jamescarter-le opened this issue Apr 15, 2024 · 4 comments
Closed

Example on how to deserialize to TEP64 dictionary. #6

jamescarter-le opened this issue Apr 15, 2024 · 4 comments

Comments

@jamescarter-le
Copy link

jamescarter-le commented Apr 15, 2024

Hello! Thank you for your excellent library.

I'm trying to pull Jetton Master data, I am using the reference specification to assist me: https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#specification

I have the following example:

First, I pull the get_jetton_data:

Info smcRouter = await _ton.TrySmcLoad(Address);
var result = await _ton.SmcRunGetMethod(smcRouter.Id, new MethodIdName("get_jetton_data"));

var stack = result.Stack.ToArray();

var totalSupply = stack[0].ToBigInteger();
var mintable = stack[1].ToBigInteger();
var adminAddress = stack[2].GetAddress();
var metadataCell = stack[3].ToBoc().RootCells[0];

Then, I want to deserialize the metadata cell to it's components, however I'm struggling to see how to do this.

Initially I want to decompose it to first grab the Decimals, where the case is there are onchain properties for, I am using this jUSDT token to test this:

https://tonscan.com/EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA?tab=methods

This reports the following for jetton_content:

{
  "type": "onchain",
  "data": {
    "image": "https://bridge.ton.org/token/1/0xdac17f958d2ee523a2206206994597c13d831ec7.png",
    "uri": "https://bridge.ton.org/token/1/0xdac17f958d2ee523a2206206994597c13d831ec7.json",
    "decimals": "6"
  }
}

Now I want to deserialize my metadataCell into a dictionary for me to extract the above representation, but I can't see how to do this. I can call LoadDict on the cell, which it should be, I get back a Cell.
I then use the specification referenced for Token Data, that the key should be a SHA256 as a string, and the value should be serialized later (so I pull the Slice). This isn't working for me and fails to deserialize the dictionary (No dictionary found).

public static Metadata ParseFromCell(Cell cell)
{
    const int OnchainContent = 0;
    const int OffchainContent = 1;

    var slice = cell.BeginRead();
    var contentType = slice.LoadByte();

    if(contentType != OnchainContent)
        throw new Exception("Unsupported representation type");

    var dict = slice.LoadDict();
    var output = dict.BeginRead().LoadAndParseDict<string, TonLibDotNet.Cells.Slice>(
        256,
        x => x.LoadString()!,
        x => x,
        new StringComparer());

    return new Metadata();
}

Do you have any suggestions on how I can get this deserialized correctly, or at least get a reference to a Dictionary<TKey,TValue> for me to take this further?

@jamescarter-le
Copy link
Author

jamescarter-le commented Apr 15, 2024

I was able to figure this out for the following cases:

Where the Content Representation is On-chain content layout or Off-chain content layout
Where the data stored, is serialized in snake case

You can call HydrateFromUri to pull additional attributes that will provide missing data to the metadata.

    public record JettonMetadata
    {
        private const int DefaultDecimals = 9;
        private const string KeyHashUri = "70e5d7b6a29b392f85076fe15ca2f2053c56c2338728c4e33c9e8ddb1ee827cc";
        private const string KeyHashName = "82a3537ff0dbce7eec35d69edc3a189ee6f17d82f353a553f9aa96cb0be3ce89";
        private const string KeyHashDescription = "c9046f7a37ad0ea7cee73355984fa5428982f8b37c8f7bcec91f7ac71a7cd104";
        private const string KeyHashImage = "6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d";
        private const string KeyHashImageData = "d9a88ccec79eef59c84b671136a20ece4cd00caaad5bc47e2c208829154ee9e4";
        private const string KeyHashSymbol = "b76a7ca153c24671658335bbd08946350ffc621fa1c516e7123095d4ffd5c581";
        private const string KeyHashDecimals = "ee80fd2f1e03480e2282363596ee752d7bb27f50776b95086a0279189675923e";
        private const string KeyHashAmountStyle = "8b10e058ce46c44bc1ba139bc9761721e49170e2c0a176129250a70af053b700";
        private const string KeyHashRenderType = "d33ae06043036d0d1c3be27201ac15ee4c73da8cdb7c8f3462ce308026095ac0";

        private bool isHydrated = false;

        public string? Uri { get; init; }
        public string? Name { get; init; }
        public string? Description { get; init; }
        public string? Image { get; init; }
        public byte[]? ImageData { get; init; }
        public string? Symbol { get; init; }
        public int Decimals { get; init; } = DefaultDecimals;
        public string? RenderType { get; init; }
        public string? AmountStyle { get; init; }

        public bool CanHydrate => Uri != null && isHydrated == false;

        public static JettonMetadata ParseFromCell(Cell cell)
        {
            const int OnchainContent = 0;
            const int OffchainContent = 1;

            var slice = cell.BeginRead();
            var contentType = slice.LoadByte();

            if (contentType == OnchainContent)
            {
                return FromOnchainContent(slice);
            }
            else if (contentType == OffchainContent)
            {
                var uri = slice.LoadString();
                return new JettonMetadata { Uri = uri };
            }

            throw new Exception("Unsupported representation type");
        }

        private static JettonMetadata FromOnchainContent(Slice slice)
        {
            var onchainData = slice.LoadAndParseDict(
                256,
                x => Convert.ToHexString(x.LoadBytes(32)).ToLower(),
                x => x,
                StringComparer.Instance);

            var metadata = new JettonMetadata();

            string? GetString(string key)
            {
                if (onchainData.TryGetValue(key, out var value))
                {
                    var format = value.LoadByte();
                    return format switch
                    {
                        0 => value.LoadString(),
                        1 => throw new NotImplementedException("Not yet support Chunked Format https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#specification"),
                        _ => throw new Exception("Unknown data serialization format https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md#specification")
                    };
                }

                return null;
            }

            int decimals = DefaultDecimals;
            var decimalsStr = GetString(KeyHashDecimals);
            if (decimalsStr != null)
                decimals = int.Parse(decimalsStr);

            metadata = new JettonMetadata
            {
                Decimals = decimals,
                Uri = GetString(KeyHashUri),
                Name = GetString(KeyHashName),
                Description = GetString(KeyHashDescription),
                Image = GetString(KeyHashImage),
                Symbol = GetString(KeyHashSymbol),
                AmountStyle = GetString(KeyHashAmountStyle),
                RenderType = GetString(KeyHashRenderType)
            };

            return metadata;
        }

        public async Task<JettonMetadata> HydrateFromUri(HttpClient? httpClient = null)
        {
            if(string.IsNullOrEmpty(Uri))
                throw new InvalidOperationException("Uri is not set");

            httpClient ??= new HttpClient();
            var stream = await httpClient.GetStreamAsync(Uri);
            var jsonDoc = JsonDocument.Parse(stream);

            var result = this with { isHydrated = true };

            if (string.IsNullOrEmpty(Name) && jsonDoc.RootElement.TryGetProperty("name", out var name))
                result = result with { Name = name.GetString() };
            if (string.IsNullOrEmpty(Description) && jsonDoc.RootElement.TryGetProperty("description", out var description))
                result = result with { Description = description.GetString() };
            if (string.IsNullOrEmpty(Image) && jsonDoc.RootElement.TryGetProperty("image", out var image))
                result = result with { Image = image.GetString() };
            if (string.IsNullOrEmpty(Symbol) && jsonDoc.RootElement.TryGetProperty("symbol", out var symbol))
                result = result with { Symbol = symbol.GetString() };
            if (string.IsNullOrEmpty(RenderType) && jsonDoc.RootElement.TryGetProperty("render_type", out var renderType))
                result = result with { RenderType = renderType.GetString() };
            if (string.IsNullOrEmpty(AmountStyle) && jsonDoc.RootElement.TryGetProperty("amount_style", out var amountStyle))
                result = result with { AmountStyle = amountStyle.GetString() };

            return result;
        }
    }
}

public class StringComparer : IEqualityComparer<string>
{
    public static readonly StringComparer Instance = new();
    public bool Equals(string? x, string? y) => x == y;
    public int GetHashCode(string obj) => obj.GetHashCode();
}

@justdmitry
Copy link
Owner

Hi,
Yes, using ...ParseDict with slices is the correct way, your solution seems fine in general.
Also, please note that v0.21.3 introduced ...ParseDictRef function that gives you value as Cell (instead of Slice), which is useful when dict is stored inside dict (or other cases when dict_set_ref in FunC had been used).

Some minor fixes in your code:

else if (contentType == OffchainContent)
{
    var uri = slice.LoadString();
    return new JettonMetadata { Uri = uri };
} 

I think it should be slice.LoadStringSnake() here, and in 0 => value.LoadString(), too.

BTW, LoadStringSnake takes a parameter to omit/skip check of 0x00 prefix (which I saw missing in many places).

.

httpClient ??= new HttpClient();

Don't forget to dispose self-created HttpClient

@jamescarter-le
Copy link
Author

Oh nice thanks for the tips!

Yes temp HttpClient should be disposed. Appreciate the library.

Off topic: Is there a way to subscribe to Blocks/Transactions using tonlib, such that I can grab Transactions for Accounts as they are emitted?

@justdmitry
Copy link
Owner

Is there a way to subscribe to Blocks/Transactions using tonlib, such that I can grab Transactions for Accounts as they are emitted?

No, there is no such functionality in tonlib.
You need either to use some HTTP API (https://docs.ton.org/develop/dapps/apis/) that supports blocks/transactions streaming, or request account tx/status every several seconds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants