[Symbol] Pythonで複数人に同時にxymを送金する

# 概要
- 秘密鍵を有するウォレットから任意の複数アドレスへ同時にモザイク(xym)を送金プログラムです
    - <font color="red">**注意**</font> : 秘密鍵は絶対に公開しないでください
- 全コードは[Githubにて公開中](https://github.com/ancte0721/symbol-python-training)
    - 本記事は「05_aggregateTransaction1」
    - バグや次回の要望あればコメントください
- 投げXYMは「**NDLS6GYOIPHATATNAVVOUNJXBD6X4BXU6IRBHIY**」まで
- ノード運営しています。あなたの委任お待ちしております。
    - **symbol-node.takagi-tech.com**
    - ハーベスト報酬の99%を還元しています。
    - [詳細はこちら](https://qiita.com/nobu_kyutech/items/19e406a00057f5b54c98)

## 前提知識
前回記事のトランザクションの生成からノードへのアナウンスが理解できている方向けの記事になります。
まだご覧になっていない方は先にこちらをご覧ください。

https://qiita.com/nobu_kyutech/items/049971f8b82cc9a055e8

## アグリゲートトランザクションとは
複数のトランザクションを内包するトランザクションをアグリゲートトランザクションと呼びます。
今回触れないことも多くありますが、こちらがとても参考になります。

https://www.cryptostream.jp/nem-symbol-aggregate-transaction-11404/

# 各種設定の確認
## 送信先一覧の設定
今回は下記の3人に送金します。
xymの数量、メッセージを個別に設定できるようにしています。
1人目には1xym、2人目には2xym、3人目には3xymの計6xymを送金します。

```python
# 送信先アドレス、送金量、メッセージ（送金量は可分性が6なので1000000倍した数値を指定）
TO_LIST = [
    ["TBMAGI3FR6ZFDXB5CHYTCE5EBKBSQP2CZPS7CGA", 1 * 1000000, "Hi Bob."],
    ["TAJEWDXCCMQLWTKVWDAX2EW7KS3L2NT6BOOIDSQ", 2 * 1000000, "Hi Alice."],
    ["TBQFD5L6OPJHHNJWCANKH6GG4GF3RRZOXNQ4KUA", 3 * 1000000, "こんにちは、太郎さん"]
]
```

## その他設定
宛先以外は前回記事と同じ内容になります。

```python
# 送信元
FROM_PRIVATEKEY = "270A5053FEDDAD47B05216D270A2F9E6DDCE74A8A16E8E3410022BDE011A3***"

# 最大手数料 (この例だと0.1枚)
MAX_FEE = 1 * 100000

# Symbol誕生のUTC秒 (メインネットは1615853185)
BIRTHTIME = 1616694977

# トランザクションの有効期限(単位はhour。この例だと2時間)
EXP_TIME = 2

# XYMのモザイクID (メインネットは0x6BED913FA20223F8)
MOSAIC_ID = 0x091F837E059AE13C

# トランザクションの送信先ノード
NODEURL = "http://sym-test-01.opening-line.jp:3000"
```

# トランザクションの作成・署名とノードへアナウンス

1. 3人に対するそれぞれの個別トランザクションの生成
1. 3つのトランザクションを内包するアグリゲートトランザクションの作成
1. アグリゲートトランザクションへ署名
1. ノードへアナウンス

を行います。

```python
import datetime
import urllib.request
import json
from binascii import unhexlify, hexlify
from symbolchain.core.CryptoTypes import PrivateKey, Hash256
from symbolchain.core.sym.KeyPair import KeyPair
from symbolchain.core.facade.SymFacade import SymFacade
from symbolchain.core.sym.MerkleHashBuilder import MerkleHashBuilder
import sha3

facade = SymFacade('public_test')
keypair = KeyPair(PrivateKey(unhexlify(FROM_PRIVATEKEY)))

# トランザクションのリスト作成
tx_lst = []
for item in TO_LIST:
    tx = facade.transaction_factory.create_embedded({
        'type': 'transfer',
        'signer_public_key': keypair.public_key,
        'recipient_address': SymFacade.Address(item[0]),
        'mosaics': [(MOSAIC_ID, item[1])],
        'message': item[2]
    })
    tx_lst.append(tx)

# マークルハッシュの作成
hash_builder = MerkleHashBuilder()
for tx in tx_lst:
    hash_builder.update(Hash256(sha3.sha3_256(tx.serialize()).digest()))
merkle_hash = hash_builder.final()

# アグリゲートトランザクションの有効期限
deadline = (int((datetime.datetime.today() + datetime.timedelta(hours=EXP_TIME)).timestamp()) - BIRTHTIME) * 1000

# アグリゲートコンプリートの作成
aggregate = facade.transaction_factory.create({
    'type': 'aggregateComplete',
    'signer_public_key': keypair.public_key,
    'fee': MAX_FEE,
    'deadline': deadline,
    'transactions_hash': merkle_hash,
    'transactions': tx_lst
})

# 自己署名
signature = facade.sign_transaction(keypair, aggregate)
aggregate.signature = signature.bytes

# アグリゲートトランザクションのハッシュ値
tx_hash = facade.hash_transaction(aggregate)

# ノードへアナウンス
payload = {"payload": hexlify(aggregate.serialize()).decode('utf8').upper()}
req = urllib.request.Request(NODEURL + "/transactions",
                             json.dumps(payload).encode(),
                             {'Content-type': 'application/json'},
                             method='PUT')
with urllib.request.urlopen(req) as res:
    print("tx hash:" + str(tx_hash))
    print("status code:" + str(res.getcode()))
    print('http://explorer.testnet.symboldev.network/transactions/' + str(tx_hash))
```

問題なく実行できれば、下記のように `202` がAPIから返ってきます。

```
tx hash:CD321C0840D9DFCEE419A2D3C2281BFC818963F3C836F51CB47C0237E8E85DC0
status code:202
http://explorer.testnet.symboldev.network/transactions/CD321C0840D9DFCEE419A2D3C2281BFC818963F3C836F51CB47C0237E8E85DC0
```

エクスプローラーからも確認できますね。
これで3人に同時に送金が完了しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183156/c45714a0-ef80-6abb-6bc4-482dace74ba2.png)


# トランザクション内容の確認
次に、受理されたトランザクション内容を確認したいと思います。
自分宛のトランザクションが含まれるアグリゲートトランザクションの場合、

1. 自分宛のトランザクション一覧からアグリゲートトランザクションIDを取得
1. そのアグリゲートトランザクションの詳細を取得

のように2段階で問い合わせが必要になります。
上記のサンプルの受信者の1人 `TBMAGI3FR6ZFDXB5CHYTCE5EBKBSQP2CZPS7CGA` で確認してみます。

## アグリゲートトランザクションIDの取得
自分宛のトランザクションが含まれるアグリゲートトランザクション一覧を確認していきます。
問い合わせ時のパラメーターに `'type': tx_type,` を含めることでアグリゲートトランザクションのみ取得します。
`print(d['id'])` でアグリゲートトランザクションIDを表示します。

```python
# メッセージ確認対象のアドレス
TO_ADDRESS = "TBMAGI3FR6ZFDXB5CHYTCE5EBKBSQP2CZPS7CGA"
# トランザクションのタイプ(0x4141 (16705 decimal) - AggregateCompleteTransaction.)
tx_type = 0x4141

req = urllib.request.Request(NODEURL + '/accounts/' + TO_ADDRESS)
with urllib.request.urlopen(req) as res:
    accountInfo = json.load(res)
ADDRESS48 = accountInfo['account']['address']

url = NODEURL + '/transactions/confirmed'
params = {
    'address': TO_ADDRESS,
    'type': tx_type,
    'order': 'desc',
}

req = urllib.request.Request('{}?{}'.format(url, urllib.parse.urlencode(params)))
with urllib.request.urlopen(req) as res:
    data = json.load(res)

for d in data['data']:
    print(d['id'])
```

結果は

```
60FFF136A36C2904DF689E49
60FFF089A36C2904DF689E35
```

2件ありました。

## アグリゲートトランザクション詳細の取得
先ほどの結果のうち、 `60FFF136A36C2904DF689E49` の詳細を確認します。
自分宛ではないトランザクションや、送金以外のトランザクションも含まれるので、下記のように絞り込みをかけます。

```python
# トランザクションのタイプ(0x4154 (16724 decimal) - TransferTransaction.)
tx_type = 0x4154
# XYMのモザイクID(メインネットは6BED913FA20223F8)
XYM_ID = '091F837E059AE13C'

# 詳細を取得するアグリゲートトランザクションのid
tx_ids = ['60FFF136A36C2904DF689E49']

url = NODEURL + '/transactions/confirmed'
params = {
    'transactionIds': tx_ids
}
req = urllib.request.Request(url,
                             json.dumps(params).encode(),
                             {'Content-type': 'application/json'},
                             method='POST')
with urllib.request.urlopen(req) as res:
    data = json.load(res)

for d in data:
    for tx in d['transaction']['transactions']:
        # 自分宛の送金トランザクションのみ抽出
        if tx['transaction']['type'] == tx_type and tx['transaction']['recipientAddress'] == ADDRESS48:
            message = ""
            if "message" in tx['transaction']:
                message = tx['transaction']['message']
            # XYMの受信のみ抽出
            for mosaic in tx['transaction']['mosaics']:
                if mosaic['id'] == XYM_ID:
                    print("height:{}, {}, {}xym, [{}]".format(tx['meta']['height'], '受信' , int(mosaic['amount'])/1000000, unhexlify(message).decode('utf-8')))
    
```

結果はこのようになり、先ほど実行したサンプル通り、1xym + メッセージが確認できました。

```
height:275528, 受信, 1.0xym, [Hi Bob.]
```

**全コードは[こちら](https://github.com/ancte0721/symbol-python-training/blob/master/05_aggregateTransaction1.ipynb)**

# 参考情報
- [Symbolブロックチェーンで一人が複数人に送金するトランザクションをPythonで作成する](https://qiita.com/nem_takanobu/items/d148dee444a74737769b)
- [ENTERキー1発🚀Pythonで複数アドレスに一斉送金【実行編】](https://nemlog.nem.social/blog/62080)