Symbolのハーベスト設定をワントランザクションで行う 


この記事では、Symbolドキュメントの[デリゲートハーベスティングを手動で有効化](https://docs.symbolplatform.com/ja/guides/harvesting/activating-delegated-harvesting-manual.html)を少しだけわかりやすく説明します。

この記事は、Symbol保有者のうち、1万XYM以上保有かつインポータンスが0%でない、いわゆるハーベスター向けの記事です。


Symbolではノードを保有していなくとも[デリゲートハーベスティング](https://docs.symbolplatform.com/ja/concepts/harvesting.html#delegated-harvesting)という方法でハーベストを行うことができます。（ハーベストについては別途解説記事が沢山あるのでここでは省きます）

このハーベストを行うためには、基本的にはSymbolウォレット（デスクトップ・モバイル等）を使用して事前にハーベスト設定を行う必要があります。ただ、みなさんもご存知の通り、一部のウォレットを除きハーベスト設定は何回かパスワードを入れないといけなくて煩雑だったり、ハーベストのリンクトランザクションの合間を狙われてアグリゲートボンデッド詐欺が発生したり、と少し状況がよくありません。

今回はこのハーベスト設定を行う流れの理解と、実際にワントランザクション（アグリゲートトランザクション）でハーベスト設定を済ませてしまう方法をご紹介します。

Symbolドキュメントの解説
===

それでは、まずSymbolドキュメントの簡単な解説から行っていきます。

https://docs.symbolplatform.com/ja/guides/harvesting/activating-delegated-harvesting-manual.html#activating-delegated-harvesting-manually

Symbolでノードを保有せずにハーベスティングを行うハーベスト方法は「デリゲートハーベスティング」という方法を利用します。
デリゲートハーベスティングを行うため必要な手順は下記です。（ドキュメントから引用）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/1b6e5558-42a1-55bf-01b1-e1fa6bf0f5b5.png)


## ここで登場するアカウント

* メインアカウント（委任するアカウント）
* リモートアカウント
* VRF アカウント
* ノードのTLSキー（ノードのパブリックキー）

各アカウントの詳細は小難しいので省きますが、大事なのはデリゲートハーベスティングを行うためには上記 **４つのアカウント** が必要と言うことです。

## ハーベスト設定の流れ

それでは実際にハーベスト設定を行う流れを説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/1b6e5558-42a1-55bf-01b1-e1fa6bf0f5b5.png)

簡単に説明すると以下のようになります。

1. メインアカウントのインポータンスをリモートアアカウントに移譲
2. メインアカウントをVRF アカウントにリンク
3. メインアカウントをノードにリンク
4. リモートアカウントの秘密鍵をノードにリクエスト

これらは全て下記のトランザクションで実行します。 

1. 「AccountKeyLinkTransaction 」
2. 「VrfKeyLinkTransaction」
3. 「NodeKeyLinkTransaction 」
4. 「Persistent Delegation Request Transaction」

各トランザクションの詳細はドキュメントから参照してください[（ドキュメントへのリンク）](https://docs.symbolplatform.com/ja/guides/harvesting/activating-delegated-harvesting-manual.html#summary)

* リモートアカウント
* VRFアカウント

この2つのアカウントに関しては新規作成して設定を行います。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/0b9c1360-add0-8413-15bb-f913748f29bb.png)

なぜ新規作成するかと言うと、この2つのアカウントは **トランザクションを送受信したことがない** 必要があるからです。
もちろん、トランザクションを送受信したことのないアカウントの秘密鍵を持ってる場合はそれを使うことも出来ますが、新規作成した方が楽ですのでここでは新規作成を行います。（リモートアカウントとVRFアカウントの秘密鍵を持っている必要がある）

## 実際のコード

一部変数は事前に取得済とします（最後に全体のコードを掲載していますので詳しくはそちらをご覧ください）

### メインアカウントのインポータンスをリモートアアカウントに移譲するためのAccountKeyLinkTransaction

```javascript
//AccountKeyLinkTransaction （リンク）
const accountLinkTransaction = symbolSdk.AccountKeyLinkTransaction.create(
    symbolSdk.Deadline.create(epochAdjustment),
    remoteAccount.publicKey,
    symbolSdk.LinkAction.Link,
    networkType,
);
```


### メインアカウントをVRF アカウントにリンクするためのVrfKeyLinkTransaction 

```javascript
//VrfKeyLinkTransaction （リンク）
const vrfLinkTransaction = symbolSdk.VrfKeyLinkTransaction.create(
  symbolSdk.Deadline.create(epochAdjustment),
  vrfAccount.publicKey,
  symbolSdk.LinkAction.Link,
  networkType,
);
```


### メインアカウントをノードにリンクするためのNodeKeyLinkTransaction

```javascript
//NodeKeyLinkTransaction （リンク）
const nodeLinkTransaction = symbolSdk.NodeKeyLinkTransaction.create(
  symbolSdk.Deadline.create(epochAdjustment),
  nodeInfo.nodePublicKey,
  symbolSdk.LinkAction.Link,
  networkType,
);
```

### リモートアカウントの秘密鍵をノードにリクエストするためのPersistentDelegationRequestTransaction

```javascript
//PersistentDelegationRequestTransactionを作成
const persistentDelegationRequestTransaction = symbolSdk.PersistentDelegationRequestTransaction.createPersistentDelegationRequestTransaction(
  symbolSdk.Deadline.create(epochAdjustment),
  remoteAccount.privateKey,
  vrfAccount.privateKey,
  nodeInfo.nodePublicKey,
  networkType,
);
```

### 上記4つのトランザクションをアグリゲートにまとめます

```javascript
//アグリゲートでまとめる
const aggregateTransaction = symbolSdk.AggregateTransaction.createComplete(
  symbolSdk.Deadline.create(epochAdjustment),
  [
    accountLinkTransaction.toAggregate(account.publicAccount),
    vrfLinkTransaction.toAggregate(account.publicAccount),
    nodeLinkTransaction.toAggregate(account.publicAccount),
    persistentDelegationRequestTransaction.toAggregate(account.publicAccount),
  ]
  networkType,
  [],
).setMaxFeeForAggregate(medianFeeMultiplier, 1);
```

アグリゲートトランザクションへ追加する順番に注意してください。

1. accountLinkTransaction
2. vrfLinkTransaction
3. nodeLinkTransaction
4. persistentDelegationRequestTransaction

の順番です。アグリゲートトランザクションは追加した順番に処理されるので間違えると正常に委任されません。

### トランザクションへ署名

アグリゲートトランザクションへ署名を行います。署名するためには秘密鍵が必要です。

```javascript
//署名
const signedTransaction = account.sign(aggregateTransaction, networkGenerationHash);
```

### ネットワークへアナウンス

最後にネットワークにアナウンスして完了です。

```javascript
//トランザクションをネットワークにアナウンス
transactionRepository.announce(signedTransaction).subscribe((x)=>{console.log(x)},(err)=>{console.log(err)});
```

https://testnet.symbol.fyi/transactions/5E69EF7B5515ABBCA8164B85645A8EB2838FFA697BC26B1211A6BE66A844071B

# やってみる

## 必要なもの

* PC
* インターネット接続環境
* テキストエディタ（VSCodeを推奨）
* Google Chromeブラウザ
* めげない心

それでは今まで解説した情報をもとに全ソースコードを掲載します。
下記のソースコードでは委任状態を取得して既にどこかに委任してる場合は一旦アンリするトランザクションも追加してあります。


```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  <title>ワンクリックハーベスト設定</title>
</head>
<body>
  <div class="container">
    <div class="card mt-3 mb-3">
      <div class="card-body">
        <div class="mb-3">
          <label for="node" class="form-label">委任先ノード</label>
          <input type="text" class="form-control" id="node" placeholder="例）yamatanoorochi.sfn.tools https://や:3001などは不要 httpsに対応したノードのみ">
          <label for="privateKey" class="form-label">秘密鍵</label>
          <input type="password" class="form-control" id="privateKey">
          <div class="alert alert-primary mt-3" role="alert">
            <div id="tx"></div>
          </div>
          <div class="d-grid gap-2 col-6 mx-auto mt-3">
            <button class="btn btn-primary" id="txBtn" type="button">１．トランザクション生成</button>
          </div>
        </div>
      </div>
    </div>
    <div class="card mt-3">
      <div class="card-body">
        <div class="alert alert-primary mt-3" role="alert">
          <div id="ex"><a href="#" target="_blank" rel="noopener noreferrer"></a></div>
        </div>
        <div class="mb-3">
          <div class="d-grid gap-2 col-6 mx-auto mt-3">
            <button class="btn btn-primary" id="networkSendBtn" type="button">２．トランザクションをネットワークにアナウンス</button>
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  <script src="https://xembook.github.io/nem2-browserify/symbol-sdk-1.0.2.js"></script>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script>
    const symbolSdk = require("/node_modules/symbol-sdk");
    /** === テストネット === **/
    const node = "https://yamatanoorochi.sfn.tools:3001";
    const mosaic_id = "3A8416DB2D53B6C8";
    const networkType = symbolSdk.NetworkType.TEST_NET;
    const explorer = "https://testnet.symbol.fyi/transactions/";
    /** === テストネット === **/
    /** === メインネット === **/
//    const node = "https://dual-1.nodes-xym.work:3001";
//    const mosaic_id = "6BED913FA20223F8";
//    const networkType = symbolSdk.NetworkType.MAIN_NET;
//    const explorer = "https://symbol.fyi/transactions/";
    /** === メインネット === **/
    const repositoryFactory = new symbolSdk.RepositoryFactoryHttp(node);
    const transactionRepository = repositoryFactory.createTransactionRepository();
    const networkRepository = repositoryFactory.createNetworkRepository();
    const accountRepository = repositoryFactory.createAccountRepository();
    let networkGenerationHash = null;
    let medianFeeMultiplier = null;
    let epochAdjustment = null;
    let signedTransaction = null;

    $(async () => {
      // GenerationHashの取得
      networkGenerationHash = await repositoryFactory.getGenerationHash().toPromise();
      // 最小手数料係数を取得
      medianFeeMultiplier = (await networkRepository.getTransactionFees().toPromise()).medianFeeMultiplier;
    });

    /**
     * 1. トランザクション生成ボタンの処理
     * **/
    $('#txBtn').on('click', async function(){
      
      //委任先ノードの取得
      const node = $('#node').val();
      //委任アドレスの取得
      const privateKey = $('#privateKey').val();
      const account = symbolSdk.Account.createFromPrivateKey(privateKey, networkType);
      const accountInfo = await accountRepository.getAccountInfo(account.address).toPromise();
      
      let transactionList = [];

      //ノードのパプリックキーを取得
      const nodeHttp = new symbolSdk.NodeHttp('https://' + node + ':3001');
      const nodeInfo = await nodeHttp.getNodeInfo().toPromise();

      // epochAdjustmentの取得
      epochAdjustment = await repositoryFactory.getEpochAdjustment().toPromise();

      //リモートアカウントの生成
      const remoteAccount = symbolSdk.Account.generateNewAccount(networkType);
      //VRFアカウントの生成
      const vrfAccount = symbolSdk.Account.generateNewAccount(networkType);


      //委任しているようであれば解除トランザクション作成
      if(accountInfo.supplementalPublicKeys.linked){
        //AccountKeyLinkTransaction （解除）
        const accountUnLinkTransaction = symbolSdk.AccountKeyLinkTransaction.create(
          symbolSdk.Deadline.create(epochAdjustment),
          accountInfo.supplementalPublicKeys.linked.publicKey,
          symbolSdk.LinkAction.Unlink,
          networkType,
        );
        transactionList.push(accountUnLinkTransaction.toAggregate(account.publicAccount));
      }

      if(accountInfo.supplementalPublicKeys.vrf){
        //VrfKeyLinkTransaction （解除）
        const vrfUnLinkTransaction = symbolSdk.VrfKeyLinkTransaction.create(
          symbolSdk.Deadline.create(epochAdjustment),
          accountInfo.supplementalPublicKeys.vrf.publicKey,
          symbolSdk.LinkAction.Unlink,
          networkType,
        );
        transactionList.push(vrfUnLinkTransaction.toAggregate(account.publicAccount));
      }

      if(accountInfo.supplementalPublicKeys.node){
        //NodeKeyLinkTransaction （解除）
        const nodeUnLinkTransaction = symbolSdk.NodeKeyLinkTransaction.create(
          symbolSdk.Deadline.create(epochAdjustment),
          accountInfo.supplementalPublicKeys.node.publicKey,
          symbolSdk.LinkAction.Unlink,
          networkType,
        );
        transactionList.push(nodeUnLinkTransaction.toAggregate(account.publicAccount));
      }

       //AccountKeyLinkTransaction （リンク）
       const accountLinkTransaction = symbolSdk.AccountKeyLinkTransaction.create(
        symbolSdk.Deadline.create(epochAdjustment),
        remoteAccount.publicKey,
        symbolSdk.LinkAction.Link,
        networkType,
      );
      transactionList.push(accountLinkTransaction.toAggregate(account.publicAccount));

      //VrfKeyLinkTransaction （リンク）
      const vrfLinkTransaction = symbolSdk.VrfKeyLinkTransaction.create(
        symbolSdk.Deadline.create(epochAdjustment),
        vrfAccount.publicKey,
        symbolSdk.LinkAction.Link,
        networkType,
      );
      transactionList.push(vrfLinkTransaction.toAggregate(account.publicAccount));

      //NodeKeyLinkTransaction （リンク）
      const nodeLinkTransaction = symbolSdk.NodeKeyLinkTransaction.create(
        symbolSdk.Deadline.create(epochAdjustment),
        nodeInfo.nodePublicKey,
        symbolSdk.LinkAction.Link,
        networkType,
      );
      transactionList.push(nodeLinkTransaction.toAggregate(account.publicAccount));

      //PersistentDelegationRequestTransactionを作成
      const persistentDelegationRequestTransaction = symbolSdk.PersistentDelegationRequestTransaction.createPersistentDelegationRequestTransaction(
        symbolSdk.Deadline.create(epochAdjustment),
        remoteAccount.privateKey,
        vrfAccount.privateKey,
        nodeInfo.nodePublicKey,
        networkType,
      );
      transactionList.push(persistentDelegationRequestTransaction.toAggregate(account.publicAccount));

      //アグリゲートでまとめる
      const aggregateTransaction = symbolSdk.AggregateTransaction.createComplete(
        symbolSdk.Deadline.create(epochAdjustment),
        transactionList,
        networkType,
        [],
      ).setMaxFeeForAggregate(medianFeeMultiplier, 1);

      //署名
      signedTransaction = account.sign(aggregateTransaction, networkGenerationHash);
      $('#tx').text(
        `hash：${signedTransaction.hash}`+"\n"+
        `payload：${signedTransaction.payload}`
      );
      $('#txBtn').prop("disabled", true);
      $('#networkSendBtn').prop("disabled", false);
    });


    /**
     * 2.トランザクションをネットワークにアナウンス
     * **/
    $('#networkSendBtn').on('click', function(){
      //トランザクションをネットワークにアナウンス
      transactionRepository.announce(signedTransaction).subscribe((x)=>{
        $("#ex a").attr("href", explorer+signedTransaction.hash);
        $("#ex a").text(explorer+signedTransaction.hash);
        console.log(x)
      },(err)=>{console.log(err)});
    });

  </script>
</body>
</html>
```

コードをindex.htmlなどのファイル名にしてデスクトップへ保存し、Goole ChromeなどのWEBブラウザへドラッグアンドドロップすれば自分のPCでもワンクリックハーベスト設定が行なえます。
「ノード」には委任したいノードのURL（FQDN）をコピペなどで入力してください。
（図ではノードとなっていますが、ソースコードでは委任先ノードとなっています）
「秘密鍵」は委任したいアカウントの秘密鍵をコピペで入力してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/64ded2c3-4883-e69a-a919-65e31fd0b96e.png)

トランザクションを生成した際のイメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/03670912-004f-f5e3-a316-aaa19cede8cf.png)

トランザクションをネットワークへアナウンスした際のイメージ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/666311ca-8065-3add-e5d0-46d2ecf1dcae.png)


既に委任してるアカウントで実行すると下記のようにアンリンクを行ってからリンクを行います。

https://testnet.symbol.fyi/transactions/CD80CF2B9AF3C38CBA3B47C0A6B1D79C81270967A6277737BF0C241D449E954D

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/934471/0f44211d-ff03-0e46-d4ea-e8e5fe2068f9.png)

# 最後に

最後に、トランザクションを発生させるためにはアカウントの秘密鍵が必要になります。
くれぐれも他人に見せたり、教えたりしないように注意してください。

また、上記のサンプルを実際に使われる際は自己責任でお願いします。



# リンク

https://github.com/ishidad2/onclick_harvest/blob/main/index.html