Symbolにオンチェーン化されたTransferTransaction関数をRoslynを使って実行する


<b>「フルオンチェーンスクリプト」を動的コンパイルしてC#で実行してみた。って話です。</b>

何に使えばいいのか分からないけど面白そうだからやってみた。名案があれば教えて下さい。できればスマコンライクなことがしたい。

やることは以下です

- ブロックチェーンSymbolのTransferTransactionを文字列にしてチェーン上に残す…①
- C#アプリで①をRoslynにて動的にコンパイルし、必要な情報を渡してトランザクションのアナウンスを行う

# スクリプトをフルオンチェーン！
はい、該当のコード全文です。
中身の解説は行いません、TransferTransactionやSymbolについては以下をご覧ください。

https://github.com/0x070696E65/quick_learning_symbol_csharp/blob/main/04_transaction.md

やりたいことは以下の実行です。これら引数は動的コンパイルしたあとにアプリケーションから渡します。
`TransferTransaction(string node, string privateKey, string recipientAddress, ulong amount, string message, string mosaicID)`
```cs
var source = @"
using System;
using System.Text;
using System.Globalization;
using System.Threading.Tasks;
using System.Net.Http;
using System.Text.Json.Nodes;
using CatSdk.Symbol;
using CatSdk.Facade;
using CatSdk.CryptoTypes;
using CatSdk.Utils;
using CatSdk.Symbol.Factory;
using NetworkTimestamp = CatSdk.Symbol.NetworkTimestamp;

public class SymbolTransactions
{
    static async Task<string> Announce(string _node, string payload)
    {
        using var client = new HttpClient();
        var content = new StringContent(payload, Encoding.UTF8, ""application/json"");
        var response =  client.PutAsync(_node + ""/transactions"", content).Result;
        return await response.Content.ReadAsStringAsync();
    }
    static async Task<string> GetDataFromApi(string _node, string _param)
    {
        var url = $""{_node}{_param}"";
        using var client = new HttpClient();
        try
        {
            var response = await client.GetAsync(url);

            if (response.IsSuccessStatusCode) {
                return await response.Content.ReadAsStringAsync();
            }
            throw new Exception($""Error: {response.StatusCode}"");
        }
        catch (Exception ex) {
            throw new Exception(ex.Message);
        }
    }
    
    public async static Task<string> TransferTransaction(string node, string privateKey, string recipientAddress, ulong amount, string message, string mosaicID)
    {
        NetworkType networkType;
        Network networkTypeForFacade;
        var s = JsonNode.Parse(await GetDataFromApi(node, ""/network/properties""));
        try
        {
            var networkIdentifier = (string) s[""network""][""identifier""];
            switch (networkIdentifier)
            {
                case ""mainnet"":
                    networkType = NetworkType.MAINNET;
                    networkTypeForFacade = Network.MainNet;
                    break;
                case ""testnet"":
                    networkType = NetworkType.TESTNET;
                    networkTypeForFacade = Network.TestNet;
                    break;
                default:
                    throw new Exception(""network is null or something wrong"");
            }
        }
        catch
        {
            throw new Exception(""network is null or something wrong"");
        }
        
        var facade = new SymbolFacade(networkTypeForFacade);
        var senderKeyPair = new KeyPair(new PrivateKey(privateKey));
        var tx = new TransferTransactionV1()
        {
            Network = networkType,
            Deadline = new Timestamp(facade.Network.FromDatetime<NetworkTimestamp>(DateTime.UtcNow).AddHours(2)
                .Timestamp),
            RecipientAddress = new UnresolvedAddress(Converter.StringToAddress(recipientAddress)),
            SignerPublicKey = senderKeyPair.PublicKey,
            Mosaics = new UnresolvedMosaic[]
            {
                new ()
                {
                    MosaicId = new UnresolvedMosaicId(ulong.Parse(mosaicID, NumberStyles.HexNumber)),
                    Amount = new Amount(amount)
                }
            },
            Message = Converter.Utf8ToPlainMessage(message)
        };
        TransactionHelper.SetMaxFee(tx, 100);
        var signature = facade.SignTransaction(senderKeyPair, tx);
        var payload = TransactionsFactory.AttachSignature(tx, signature);
        var result = await Announce( node, payload);
        return result;
    }
}
";
```

さて、これをMetalというプロトコルに沿ってオンチェーン化します。Symbolではメッセージ領域として1024byteまで使用できますが、本スクリプトは3000byteを超えたのでMetalを使います。

<b>Metalについて</b>
https://github.com/OPENSPHERE-Inc/metal-on-symbol

<b>C#用Metal</b>
https://github.com/0x070696E65/metal-on-symbol-for-csharp

```cs
var fileData = Converter.Utf8ToBytes(source);
var config = new SymbolServiceConfig("https://mikun-testnet.tk:3001");
var symbolService = new SymbolService(config);
await symbolService.Init();

var keyPair = new KeyPair(new PrivateKey("5DB8324E7EB83E7665D500B014283260EF312139034E86DFB7EE736503E*****"));
var metalService = new MetalService(symbolService);

var (key, txs, _) = await metalService.CreateForgeTxs(keyPair.PublicKey, keyPair.PublicKey, fileData);
var metalId = metalService.CalculateMetalId(
        MetadataType.Account, 
        keyPair.PublicKey,
        keyPair.PublicKey,
        key 
        );
Console.WriteLine($"MetalID: {metalId}");
var batches = symbolService.BuildSignedAggregateCompleteTxBatches(
        txs,
        keyPair
);
var totalSize = batches.Select(batch => (long)batch.Size).Sum();
var totalFee = batches.Select(batch => (long)batch.Fee.Value).Sum();
Console.WriteLine($"Total Size: {totalSize}");
Console.WriteLine($"Total Fee: {totalFee}");
var result = await symbolService.ExecuteBatches(batches);
Console.WriteLine(result);
```

この時、MetalIDをメモしておいてください。

# 実行する！

ちなみにここまではオンチェーン化なのでC#じゃなくてもいいです。ここからアプリケーションはC#で作成します。コンソールアプリで行いました。Unityなど、他では試してません。

## 必要なパッケージのインストール
Nuget等で以下のパッケージをインストールしてください
- Newtonsoft.Json
- Microsoft.CodeAnalysis.CSharp

また、こちらもそれぞれDLLファイルをダウンロードしアプリケーションに参照を追加してください。

https://github.com/0x070696E65/symbol_cs_dual_sdk/releases
- CatSdk
- BouncyCastle

https://github.com/0x070696E65/metal-on-symbol-for-csharp/releases
- MetalOnSymbolForCsharp

また、以下をダウンロードして適当な箇所に置いてください、プロジェクト下だと競合したりおかしくなる可能性があるのでデスクトップとかでも良い

[DLはこちらから検索してください]

https://www.dllme.com/
- System.Text.Json.dll
- netstandard.dll
- System.Runtime.dll
- System.Private.Uri.dll *v6
- System.Net.Primitives.dll *v6
最後の2つはv6を使ってください

## MetalIDからFetchしてstring型にする

```c# Program.cs
using System.Text;
using System.Globalization;
using System.Text.Json.Nodes;
using CatSdk.Symbol;
using CatSdk.Facade;
using CatSdk.CryptoTypes;
using CatSdk.Utils;
using CatSdk.Symbol.Factory;
using NetworkTimestamp = CatSdk.Symbol.NetworkTimestamp;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

var config = new SymbolServiceConfig("https://mikun-testnet.tk:3001");
var symbolService = new SymbolService(config);
await symbolService.Init();
var metalService = new MetalService(symbolService);
var metalId = "Fe9JDstanjckdjzq93jxU9B6GC4RtnZQBTb6JYcDVfZSpC";
var source = Encoding.UTF8.GetString((await metalService.FetchByMetalId(metalId)).Payload);
Console.WriteLine("source: " + source);
```

## Roslynで動的コンパイルを行い、関数を実行する

```cs Program.cs
var options = CSharpParseOptions.Default
    .WithLanguageVersion(LanguageVersion.CSharp9);

var syntaxTree = CSharpSyntaxTree.ParseText(source, options, "SymbolTransactions.cs");

var references = new MetadataReference[]
{
    MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(JsonNode).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(Task<>).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(HttpClient).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(StringContent).Assembly.Location),
    MetadataReference.CreateFromFile(Assembly.LoadFrom("CatSdk.dll").Location),
    MetadataReference.CreateFromFile("/PATH/System.Text.Json.dll"),
    MetadataReference.CreateFromFile("/PATH/netstandard.dll"),
    MetadataReference.CreateFromFile("/PATH/System.Runtime.dll"),
    MetadataReference.CreateFromFile("/PATH/System.Private.Uri.dll"),
    MetadataReference.CreateFromFile("/PATH/System.Net.Primitives.dll"),
};

var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);

var compilation = CSharpCompilation.Create(
    "SymbolTransactions.dll",
    new[] { syntaxTree },
    references,
    compilationOptions);

using var stream = new MemoryStream();
var emitResult = compilation.Emit(stream);

if (emitResult.Success)
{
    stream.Seek(0, SeekOrigin.Begin);
    var assembly = AssemblyLoadContext.Default.LoadFromStream(stream);
    var randomType = assembly.GetType("SymbolTransactions");
    var method = randomType.GetMethod("TransferTransaction");
    using var result = ((Task)(method.Invoke(null, new object[]{"https://mikun-testnet.tk:3001", "5DB8324E7EB83E7665D500B014283260EF312139034E86DFB7EE736503E*****", "TA5LGYEWS6L2WYBQ75J2DGK7IOZHYVWFWRLOFWI", (ulong)1, "Hello, Symbol", "72C0212E67A08BCE"}) ?? throw new InvalidOperationException()));
    await result;
    Console.WriteLine($"Result: {result.GetType().GetProperty("Result")?.GetValue(result)}");
}
else
{
    var failures = emitResult.Diagnostics.Where(diagnostic =>
        diagnostic.IsWarningAsError ||
        diagnostic.Severity == DiagnosticSeverity.Error);

    foreach (var diagnostic in failures)
    {
        Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
    }
}
```

参照の追加が面倒です。コンパイルした時に使用するクラスやアセンブリをここで追加します。最後の５つは、先にダウンロードしたファイルを絶対パスで指定します。
```cs
var references = new MetadataReference[]
{
    MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(JsonNode).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(Task<>).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(HttpClient).Assembly.Location),
    MetadataReference.CreateFromFile(typeof(StringContent).Assembly.Location),
    MetadataReference.CreateFromFile(Assembly.LoadFrom("CatSdk.dll").Location),
    MetadataReference.CreateFromFile("/PATH/System.Text.Json.dll"),
    MetadataReference.CreateFromFile("/PATH/netstandard.dll"),
    MetadataReference.CreateFromFile("/PATH/System.Runtime.dll"),
    MetadataReference.CreateFromFile("/PATH/System.Private.Uri.dll"),
    MetadataReference.CreateFromFile("/PATH/System.Net.Primitives.dll"),
};
```

また、こちらが実行する関数箇所と渡す引数です
```cs
var result = ((Task)(method.Invoke(null, new object[]
    {
        "https://mikun-testnet.tk:3001", 
        "5DB8324E7EB83E7665D500B014283260EF312139034E86DFB7EE736503E*****", 
        "TA5LGYEWS6L2WYBQ75J2DGK7IOZHYVWFWRLOFWI",
        (ulong)1,
        "Hello, Symbol",
        "72C0212E67A08BCE"
    }) ?? throw new InvalidOperationException()));
```

以上です！！！
問題無ければトランザクションがアナウンスされています。

# さいごに
さて、これを使って何ができるでしょうか？

いまいち思いついてないけど、この関数を使用した証明ができればゲーム等のチート対策になるような気はしています。

例えばスクリプト内で秘密鍵を保有し、署名して返せば、公開鍵で検証すれば証明にはなりますが、そもそもオンチェーン化するということは公開されるわけで誰でもその秘密鍵を知りうることができ、つまり意味がないということになります。

とりあえずスクリプトをオンチェーン化して、そのコードをC#で動的にコンパイルするということはできました。

なんか良い使いみちはないかなと考えてますが、思いつきません。