コンテンツにスキップ

マルチシグアカウントからトランザクションに署名する⚓︎

中級

このチュートリアルでは、1 XEM を、アカウント からそのアカウント自身へ送信します。XEM を送信する チュートリアルと同じ流れです。

ただし今回は、送信元アカウントが マルチシグアカウントマルチシグ とも呼ばれます)なので、自分でトランザクションを開始したり署名したりすることができません。 代わりに、連署人アカウントを使って、マルチシグアカウントに代わってトランザクションを作成し、署名します。

このチュートリアルで使うマルチシグアカウントは 2-of-2 マルチシグとして設定されています。 連署人が 2 人いて、送金を承認するには両方の署名が必要です。

連署人 0 が送金を開始し、連署人 1 が必要な 2 つ目の連署を提供します。

Multisignature TreeMultisignature AccountマルチシグアカウントCosignatory 0連署人 0Cosignatory 0->Multisignature AccountCosignatory 1連署人 1Cosignatory 1->Multisignature Account

別の方法: WebSocket

このチュートリアルでは、連署人がノードを照会して承認待ちのトランザクションを見つけます。 連署人にリアルタイムで通知する WebSocket ベースの方法については、マルチシグトランザクションフローをリッスンする チュートリアルを参照してください。

前提条件⚓︎

始める前に、次の準備をしてください。

  • 開発環境をセットアップする。 開発環境のセットアップ を参照してください。

  • 2-of-2 マルチシグアカウントを作成する。 作成するには、マルチシグを設定する チュートリアルで 1 から 2 に変更して実行します。 アカウントがすでに別の設定のマルチシグである場合は、まず 無効化 してください。

さらに、トランザクションのアナウンスと承認の方法を理解するため、XEM を送信する チュートリアルを確認してください。

完全なコード⚓︎

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey, PublicKey
from symbolchain.facade.NemFacade import NemFacade
from symbolchain.nc import Amount
from symbolchain.nem.FeeCalculator import calculate_transaction_fee
from symbolchain.nem.Network import NetworkTimestamp

NODE_URL = os.getenv('NODE_URL', 'http://libertalia.nemtest.net:7890')
print(f'Using node {NODE_URL}')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    announce_path = '/transaction/announce'
    print(f'Announcing {label} to {announce_path}')
    request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(request) as announce_response:
        result = json.loads(announce_response.read().decode())
    print(f'  Result: {result["message"]}')
    return result['message']


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    status_path = f'/transaction/get?hash={tx_hash}'
    print(f'Waiting for {label} confirmation from {status_path}')
    is_confirmed = False
    for _ in range(120):
        try:
            with urllib.request.urlopen(
                f'{NODE_URL}{status_path}'
            ) as status_response:
                confirmed = json.loads(status_response.read().decode())
                height = confirmed['meta']['height']
                print(f'{label} confirmed in block {height}')
                is_confirmed = True
                break
        except urllib.error.HTTPError:
            print('  Transaction status: pending')
        time.sleep(1)
    if not is_confirmed:
        print(f'{label} confirmation took too long.')


facade = NemFacade('testnet')

MULTISIG_PUBLIC_KEY = os.getenv(
    'MULTISIG_PUBLIC_KEY',
    'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2')
multisig_public_key = PublicKey(MULTISIG_PUBLIC_KEY)
multisig_address = facade.network.public_key_to_address(
    multisig_public_key)
print(f'Multisig public key: {multisig_public_key}')
COSIGNATORY0_PRIVATE_KEY = os.getenv(
    'COSIGNATORY0_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000002')
cosignatory0_key_pair = NemFacade.KeyPair(
    PrivateKey(COSIGNATORY0_PRIVATE_KEY))
print(f'Cosignatory 0 public key: {cosignatory0_key_pair.public_key}')
COSIGNATORY1_PRIVATE_KEY = os.getenv(
    'COSIGNATORY1_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000003')
cosignatory1_key_pair = NemFacade.KeyPair(
    PrivateKey(COSIGNATORY1_PRIVATE_KEY))
print(f'Cosignatory 1 public key: {cosignatory1_key_pair.public_key}')


try:
    # Fetch current network time
    time_path = '/time-sync/network-time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)

    # Build the inner transfer transaction
    transfer_transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v2',
        'signer_public_key': multisig_public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': multisig_address,
        'amount': 1_000_000  # 1 XEM
    })
    transfer_transaction.fee = Amount(
        calculate_transaction_fee(transfer_transaction))

    # Build the wrapper multisig transaction
    transaction = facade.transaction_factory.create({
        'type': 'multisig_transaction_v1',
        # This is the cosignatory that initiates the transfer
        'signer_public_key': cosignatory0_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'inner_transaction':
            facade.transaction_factory.to_non_verifiable_transaction(
                transfer_transaction)
    })
    transaction.fee = Amount(calculate_transaction_fee(transaction))

    # Sign and announce the multisig transaction
    signature = facade.sign_transaction(
        cosignatory0_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    print('Built multisig transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    announce_result = announce_transaction(
        json_payload, 'multisig transaction')
    # The transaction is now waiting for the second signature

    # Retrieve the pending transaction from the network
    if 'SUCCESS' == announce_result:
        cosignatory1_address = facade.network.public_key_to_address(
            cosignatory1_key_pair.public_key)
        unconfirmed_path = ('/account/unconfirmedTransactions'
            f'?address={cosignatory1_address}')
        print(f'Fetching pending transactions from {unconfirmed_path}')
        with urllib.request.urlopen(
            f'{NODE_URL}{unconfirmed_path}'
        ) as response:
            pending = json.loads(response.read().decode())['data']
        # Select the pending transaction issued by the multisig account
        inner_transaction_hash = next(
            entry['meta']['data'] for entry in pending
            if entry['transaction'].get('otherTrans', {}).get(
                'signer', '').upper() == str(multisig_public_key))
        print(f'  Inner transaction hash: {inner_transaction_hash}')

        # Build the cosignature
        cosignature = facade.transaction_factory.create({
            'type': 'cosignature_v1',
            # This is the cosignatory providing the second signature
            'signer_public_key': cosignatory1_key_pair.public_key,
            'timestamp': timestamp.timestamp,
            'deadline': deadline.timestamp,
            # Hash of the inner transfer transaction
            'other_transaction_hash': inner_transaction_hash,
            # Address of the multisig account
            'multisig_account_address': multisig_address
        })
        cosignature.fee = Amount(calculate_transaction_fee(cosignature))

        # Sign and announce the cosignature
        cosignature_signature = facade.sign_transaction(
            cosignatory1_key_pair, cosignature)
        cosignature_payload = facade.transaction_factory.attach_signature(
            cosignature, cosignature_signature)
        print('Built cosignature:')
        print(json.dumps(cosignature.to_json(), indent=2))
        cosignature_result = announce_transaction(
            cosignature_payload, 'cosignature')

        # Wait for the multisig transaction to be confirmed
        if 'SUCCESS' == cosignature_result:
            wait_for_confirmation(
                facade.hash_transaction(transaction),
                'multisig transaction')
        else:
            print(f'Transaction rejected: {cosignature_result}')

    else:
        print(f'Transaction rejected: {announce_result}')
except urllib.error.URLError as e:
    print(e.reason)

Download source

import { PrivateKey, PublicKey } from 'symbol-sdk';
import {
    NemFacade,
    NetworkTimestamp,
    calculateTransactionFee,
    models
} from 'symbol-sdk/nem';

const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    const announcePath = '/transaction/announce';
    console.log(`Announcing ${label} to ${announcePath}`);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    const result = await announceResponse.json();
    console.log('  Result:', result.message);
    return result.message;
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    const statusPath = `/transaction/get?hash=${transactionHash}`;
    console.log(`Waiting for ${label} confirmation from`, statusPath);
    let isConfirmed = false;
    for (let attempt = 1; 120 >= attempt; ++attempt) {
        const response = await fetch(`${NODE_URL}${statusPath}`);
        if (response.ok) {
            const confirmed = await response.json();
            console.log(`${label} confirmed in block`,
                confirmed.meta.height);
            isConfirmed = true;
            break;
        }
        console.log('  Transaction status: pending');
        await new Promise(resolve => { setTimeout(resolve, 1000); });
    }
    if (!isConfirmed)
        console.warn(`${label} confirmation took too long.`);
}

const facade = new NemFacade('testnet');

const MULTISIG_PUBLIC_KEY = process.env.MULTISIG_PUBLIC_KEY || (
    'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2');
const multisigPublicKey = new PublicKey(MULTISIG_PUBLIC_KEY);
const multisigAddress = facade.network.publicKeyToAddress(
    multisigPublicKey);
console.log(`Multisig public key: ${multisigPublicKey}`);
const COSIGNATORY0_PRIVATE_KEY = process.env.COSIGNATORY0_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000002');
const cosignatory0KeyPair = new NemFacade.KeyPair(
    new PrivateKey(COSIGNATORY0_PRIVATE_KEY));
console.log(`Cosignatory 0 public key: ${cosignatory0KeyPair.publicKey}`);
const COSIGNATORY1_PRIVATE_KEY = process.env.COSIGNATORY1_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000003');
const cosignatory1KeyPair = new NemFacade.KeyPair(
    new PrivateKey(COSIGNATORY1_PRIVATE_KEY));
console.log(`Cosignatory 1 public key: ${cosignatory1KeyPair.publicKey}`);


try {
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

    // Build the inner transfer transaction
    const transferTransaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v2',
        signerPublicKey: multisigPublicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        recipientAddress: multisigAddress.toString(),
        amount: 1_000_000n // 1 XEM
    });
    transferTransaction.fee = new models.Amount(
        calculateTransactionFee(transferTransaction));

    // Build the wrapper multisig transaction
    const transaction = facade.transactionFactory.create({
        type: 'multisig_transaction_v1',
        // This is the cosignatory that initiates the transfer
        signerPublicKey: cosignatory0KeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        innerTransaction: facade.transactionFactory.static
            .toNonVerifiableTransaction(transferTransaction)
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction));

    // Sign and announce the multisig transaction
    const signature = facade.signTransaction(
        cosignatory0KeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built multisig transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));
    const announceResult = await announceTransaction(
        jsonPayload, 'multisig transaction');
    // The transaction is now waiting for the second signature

    // Retrieve the pending transaction from the network
    if ('SUCCESS' === announceResult) {
        const cosignatory1Address = facade.network.publicKeyToAddress(
            cosignatory1KeyPair.publicKey);
        const unconfirmedPath = '/account/unconfirmedTransactions' +
            `?address=${cosignatory1Address}`;
        console.log('Fetching pending transactions from',
            unconfirmedPath);
        const unconfirmedResponse = await fetch(
            `${NODE_URL}${unconfirmedPath}`);
        const pending = (await unconfirmedResponse.json()).data;
        // Select the pending transaction issued by the multisig account
        const pendingEntry = pending.find(entry =>
            multisigPublicKey.toString() === (entry.transaction
                .otherTrans?.signer ?? '').toUpperCase());
        const innerTransactionHash = pendingEntry.meta.data;
        console.log('  Inner transaction hash:', innerTransactionHash);

        // Build the cosignature
        const cosignature = facade.transactionFactory.create({
            type: 'cosignature_v1',
            // This is the cosignatory providing the second signature
            signerPublicKey: cosignatory1KeyPair.publicKey.toString(),
            timestamp: timestamp.timestamp,
            deadline: deadline.timestamp,
            // Hash of the inner transfer transaction
            otherTransactionHash: innerTransactionHash,
            // Address of the multisig account
            multisigAccountAddress: multisigAddress.toString()
        });
        cosignature.fee = new models.Amount(
            calculateTransactionFee(cosignature));

        // Sign and announce the cosignature
        const cosignatureSignature = facade.signTransaction(
            cosignatory1KeyPair, cosignature);
        const cosignaturePayload = facade.transactionFactory.static
            .attachSignature(cosignature, cosignatureSignature);
        console.log('Built cosignature:');
        console.log(JSON.stringify(cosignature.toJson(), null, 2));
        const cosignatureResult = await announceTransaction(
            cosignaturePayload, 'cosignature');

        // Wait for the multisig transaction to be confirmed
        if ('SUCCESS' === cosignatureResult) {
            await waitForConfirmation(
                facade.hashTransaction(transaction).toString(),
                'multisig transaction');
        } else {
            console.log('Transaction rejected:', cosignatureResult);
        }

    } else {
        console.log('Transaction rejected:', announceResult);
    }
} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

Download source

コードの説明⚓︎

マルチシグアカウントに代わってトランザクションに署名するには、MultisigTransactionV1 にラップし、必要な連署を集めます。

このチュートリアルでは、ラップされるトランザクションは送金で、資金の出所であるマルチシグアカウントを署名者とします。 連署人 0 がラッパーに署名してアナウンスし、連署人 1 が 2 つ目の必要な連署をするまで、トランザクションは保留されます。

実際には、各連署人がそれぞれの秘密鍵だけを保持して、別のマシンで自分の処理を実行します。 このチュートリアルでは簡単にするため、両方の役割を 1 つのプログラムにまとめています。

コードでは、トランザクションをアナウンスして承認を待つ 2 つのヘルパー関数を定義します。 これらの動作の詳細については、XEM を送信する チュートリアルを参照してください。

アカウントをセットアップする⚓︎

MULTISIG_PUBLIC_KEY = os.getenv(
    'MULTISIG_PUBLIC_KEY',
    'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2')
multisig_public_key = PublicKey(MULTISIG_PUBLIC_KEY)
multisig_address = facade.network.public_key_to_address(
    multisig_public_key)
print(f'Multisig public key: {multisig_public_key}')
COSIGNATORY0_PRIVATE_KEY = os.getenv(
    'COSIGNATORY0_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000002')
cosignatory0_key_pair = NemFacade.KeyPair(
    PrivateKey(COSIGNATORY0_PRIVATE_KEY))
print(f'Cosignatory 0 public key: {cosignatory0_key_pair.public_key}')
COSIGNATORY1_PRIVATE_KEY = os.getenv(
    'COSIGNATORY1_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000003')
cosignatory1_key_pair = NemFacade.KeyPair(
    PrivateKey(COSIGNATORY1_PRIVATE_KEY))
print(f'Cosignatory 1 public key: {cosignatory1_key_pair.public_key}')
const MULTISIG_PUBLIC_KEY = process.env.MULTISIG_PUBLIC_KEY || (
    'D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2');
const multisigPublicKey = new PublicKey(MULTISIG_PUBLIC_KEY);
const multisigAddress = facade.network.publicKeyToAddress(
    multisigPublicKey);
console.log(`Multisig public key: ${multisigPublicKey}`);
const COSIGNATORY0_PRIVATE_KEY = process.env.COSIGNATORY0_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000002');
const cosignatory0KeyPair = new NemFacade.KeyPair(
    new PrivateKey(COSIGNATORY0_PRIVATE_KEY));
console.log(`Cosignatory 0 public key: ${cosignatory0KeyPair.publicKey}`);
const COSIGNATORY1_PRIVATE_KEY = process.env.COSIGNATORY1_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000003');
const cosignatory1KeyPair = new NemFacade.KeyPair(
    new PrivateKey(COSIGNATORY1_PRIVATE_KEY));
console.log(`Cosignatory 1 public key: ${cosignatory1KeyPair.publicKey}`);

このチュートリアルでは、環境変数で設定する 3 つのアカウントが必要です。 設定されていない場合は、デフォルト値を使用します。

環境変数 デフォルト値 用途
MULTISIG_PUBLIC_KEY D656..ACF2 2-of-2 マルチシグアカウント
COSIGNATORY0_PRIVATE_KEY 0000..0002 1 人目の連署人アカウント、開始者
COSIGNATORY1_PRIVATE_KEY 0000..0003 2 人目の連署人アカウント

各キーは 64 文字の 16 進数文字列です。

通常のアカウントとは異なり、マルチシグアカウントは自分でトランザクションを開始できません。 代わりに、連署人がアカウントに代わって署名します。 そのため、マルチシグアカウントの 秘密鍵 は必要なく、アカウントの識別には 公開鍵 だけで十分です。

マルチシグアカウントには、トランザクション手数料を支払うために十分な資金が必要です。 デフォルト値を使用する場合、このアカウントにはすでに資金がある可能性があります。

上記のスニペットでは、後で使用するために各連署人の キーペア と、マルチシグアカウントの アドレス を導出して保存します。

ネットワーク時刻を取得する⚓︎

    # Fetch current network time
    time_path = '/time-sync/network-time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        response_json = json.loads(response.read().decode())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

ネットワーク時刻は /time-sync/network-time GET から取得し、XEM を送信する チュートリアルで説明されている手順に従って、トランザクションの timestampdeadline フィールドを導出します。

トランザクションを構築する⚓︎

マルチシグトランザクションの内部にラップするトランザクションは 内部トランザクション と呼ばれ、このチュートリアルで使う転送や、マルチシグアカウントを設定する で使う変更など、任意の 基本トランザクション を指定できます。 マルチシグトランザクションを入れ子にすることはできません。

    # Build the inner transfer transaction
    transfer_transaction = facade.transaction_factory.create({
        'type': 'transfer_transaction_v2',
        'signer_public_key': multisig_public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'recipient_address': multisig_address,
        'amount': 1_000_000  # 1 XEM
    })
    transfer_transaction.fee = Amount(
        calculate_transaction_fee(transfer_transaction))
    // Build the inner transfer transaction
    const transferTransaction = facade.transactionFactory.create({
        type: 'transfer_transaction_v2',
        signerPublicKey: multisigPublicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        recipientAddress: multisigAddress.toString(),
        amount: 1_000_000n // 1 XEM
    });
    transferTransaction.fee = new models.Amount(
        calculateTransactionFee(transferTransaction));

内部の 転送トランザクション には次のフィールドが含まれます。

  • : 資金を送るアカウント、つまりマルチシグアカウントの 公開鍵

  • : この例では資金を送信者へ戻すため、受取人もマルチシグアカウントです。

  • : 1 XEM に相当する 1'000'000 原子単位。XEM を送信する チュートリアルで説明しています。

内部トランザクションには固有のトランザクション手数料があり、 で計算します。 ここで送る 1 XEM の手数料は 0.05 XEM で、送金手数料表 に示されています。

    # Build the wrapper multisig transaction
    transaction = facade.transaction_factory.create({
        'type': 'multisig_transaction_v1',
        # This is the cosignatory that initiates the transfer
        'signer_public_key': cosignatory0_key_pair.public_key,
        'timestamp': timestamp.timestamp,
        'deadline': deadline.timestamp,
        'inner_transaction':
            facade.transaction_factory.to_non_verifiable_transaction(
                transfer_transaction)
    })
    transaction.fee = Amount(calculate_transaction_fee(transaction))
    // Build the wrapper multisig transaction
    const transaction = facade.transactionFactory.create({
        type: 'multisig_transaction_v1',
        // This is the cosignatory that initiates the transfer
        signerPublicKey: cosignatory0KeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        innerTransaction: facade.transactionFactory.static
            .toNonVerifiableTransaction(transferTransaction)
    });
    transaction.fee = new models.Amount(
        calculateTransactionFee(transaction));

次に転送トランザクションを MultisigTransactionV1 にラップします。主なフィールドは次のとおりです。

マルチシグラッパーにも 0.15 XEM の固有のトランザクション手数料があり、手数料表 に示されています。 トランザクションが承認されると、すべての手数料と送金額がマルチシグアカウントから差し引かれます。

開始者: マルチシグトランザクションをアナウンスする⚓︎

    # Sign and announce the multisig transaction
    signature = facade.sign_transaction(
        cosignatory0_key_pair, transaction)
    json_payload = facade.transaction_factory.attach_signature(
        transaction, signature)
    print('Built multisig transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    announce_result = announce_transaction(
        json_payload, 'multisig transaction')
    # The transaction is now waiting for the second signature
    // Sign and announce the multisig transaction
    const signature = facade.signTransaction(
        cosignatory0KeyPair, transaction);
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction, signature);
    console.log('Built multisig transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));
    const announceResult = await announceTransaction(
        jsonPayload, 'multisig transaction');
    // The transaction is now waiting for the second signature

この場合、連署人 0 がマルチシグトランザクションの開始者です。 トランザクションに署名してネットワークにアナウンスします。

有効であればネットワークはトランザクションを受け付けますが、まだ承認されていません。 マルチシグアカウントには 2 つの連署が必要なのに対し 1 つしかないため、不足している連署が追加されるまでトランザクションは 未承認トランザクションプール で待機します。

より簡単な設定

マルチシグアカウントを設定する チュートリアルで作成する 1-of-2 のように、連署が 1 つだけ必要なマルチシグでは、開始した連署人の署名だけで十分です。 有効であれば、追加の手順なしでトランザクションが承認されます。

連署人: 承認待ちのトランザクションを取得する⚓︎

    # Retrieve the pending transaction from the network
    if 'SUCCESS' == announce_result:
        cosignatory1_address = facade.network.public_key_to_address(
            cosignatory1_key_pair.public_key)
        unconfirmed_path = ('/account/unconfirmedTransactions'
            f'?address={cosignatory1_address}')
        print(f'Fetching pending transactions from {unconfirmed_path}')
        with urllib.request.urlopen(
            f'{NODE_URL}{unconfirmed_path}'
        ) as response:
            pending = json.loads(response.read().decode())['data']
        # Select the pending transaction issued by the multisig account
        inner_transaction_hash = next(
            entry['meta']['data'] for entry in pending
            if entry['transaction'].get('otherTrans', {}).get(
                'signer', '').upper() == str(multisig_public_key))
        print(f'  Inner transaction hash: {inner_transaction_hash}')
    // Retrieve the pending transaction from the network
    if ('SUCCESS' === announceResult) {
        const cosignatory1Address = facade.network.publicKeyToAddress(
            cosignatory1KeyPair.publicKey);
        const unconfirmedPath = '/account/unconfirmedTransactions' +
            `?address=${cosignatory1Address}`;
        console.log('Fetching pending transactions from',
            unconfirmedPath);
        const unconfirmedResponse = await fetch(
            `${NODE_URL}${unconfirmedPath}`);
        const pending = (await unconfirmedResponse.json()).data;
        // Select the pending transaction issued by the multisig account
        const pendingEntry = pending.find(entry =>
            multisigPublicKey.toString() === (entry.transaction
                .otherTrans?.signer ?? '').toUpperCase());
        const innerTransactionHash = pendingEntry.meta.data;
        console.log('  Inner transaction hash:', innerTransactionHash);

ここで連署人 1 が処理を引き継ぎます。 連署人は /account/unconfirmedTransactions GET エンドポイントを使って、署名を待っている承認待ちのマルチシグトランザクションを見つけられます。

承認待ちの各マルチシグトランザクションのメタデータには 内部トランザクション のハッシュが含まれ、連署時に参照する値になります。

連署人は、承認待ちのマルチシグトランザクションを複数持つ可能性があります。 この例では、マルチシグアカウントが発行したトランザクションを選択します。 そのアカウントからの承認待ちトランザクションは 1 件だけであると想定し、連署人 1 はより正確に照合するための事前情報を持たないため、このチュートリアルにおいてはこの方法で十分です。

別の方法として、開始者がオフチェーンの経路で内部トランザクションのハッシュを他の連署人と共有できます。 連署人はそのハッシュを照合して、正確なトランザクションを選択できます。

ただし、実際のアプリケーションでは、発行アカウントだけでの絞り込みは不十分です。 承認待ちのトランザクションが期待したものだと保証するものはないため、連署するものを選ぶ前に、タイプ、受取人、金額など、承認待ちの各トランザクションの内容を確認してください。

連署する前に確認してください

連署する前に、必ずトランザクションの内容を確認してください。 連署は拘束力があり、取り消すことはできません。

連署人: トランザクションに連署する⚓︎

        # Build the cosignature
        cosignature = facade.transaction_factory.create({
            'type': 'cosignature_v1',
            # This is the cosignatory providing the second signature
            'signer_public_key': cosignatory1_key_pair.public_key,
            'timestamp': timestamp.timestamp,
            'deadline': deadline.timestamp,
            # Hash of the inner transfer transaction
            'other_transaction_hash': inner_transaction_hash,
            # Address of the multisig account
            'multisig_account_address': multisig_address
        })
        cosignature.fee = Amount(calculate_transaction_fee(cosignature))
        // Build the cosignature
        const cosignature = facade.transactionFactory.create({
            type: 'cosignature_v1',
            // This is the cosignatory providing the second signature
            signerPublicKey: cosignatory1KeyPair.publicKey.toString(),
            timestamp: timestamp.timestamp,
            deadline: deadline.timestamp,
            // Hash of the inner transfer transaction
            otherTransactionHash: innerTransactionHash,
            // Address of the multisig account
            multisigAccountAddress: multisigAddress.toString()
        });
        cosignature.fee = new models.Amount(
            calculateTransactionFee(cosignature));

連署人 1 は CosignatureV1 をアナウンスして、不足している署名を提供します。 連署には次の内容を指定します。

  • : 署名を提供する連署人の 公開鍵

  • : 前の手順で取得した内部転送トランザクションのハッシュ。

  • : 署名が対象とするマルチシグアカウントの アドレス

連署の手数料は 0.15 XEM です。 マルチシグトランザクションが承認されると、この手数料もマルチシグアカウントから差し引かれます。

        # Sign and announce the cosignature
        cosignature_signature = facade.sign_transaction(
            cosignatory1_key_pair, cosignature)
        cosignature_payload = facade.transaction_factory.attach_signature(
            cosignature, cosignature_signature)
        print('Built cosignature:')
        print(json.dumps(cosignature.to_json(), indent=2))
        cosignature_result = announce_transaction(
            cosignature_payload, 'cosignature')
        // Sign and announce the cosignature
        const cosignatureSignature = facade.signTransaction(
            cosignatory1KeyPair, cosignature);
        const cosignaturePayload = facade.transactionFactory.static
            .attachSignature(cosignature, cosignatureSignature);
        console.log('Built cosignature:');
        console.log(JSON.stringify(cosignature.toJson(), null, 2));
        const cosignatureResult = await announceTransaction(
            cosignaturePayload, 'cosignature');

次に連署人 1 が連署に署名してネットワークにアナウンスします。

アナウンスされた連署は、独立したトランザクションとして 未承認トランザクションプール に表示されません。 代わりに、ネットワークが承認待ちのマルチシグトランザクションへ付加します。

追加の連署が必要な設定では、トランザクションは保留されたままです。 もう一度 /account/unconfirmedTransactions GET を照会すれば、トランザクションの signatures フィールドで集まった署名を確認できます。

ただしこのチュートリアルでは、2 つ目の連署でトランザクションが完了します。トランザクションはプールを離れ、次のブロックで承認されます。

承認を待つ⚓︎

        # Wait for the multisig transaction to be confirmed
        if 'SUCCESS' == cosignature_result:
            wait_for_confirmation(
                facade.hash_transaction(transaction),
                'multisig transaction')
        else:
            print(f'Transaction rejected: {cosignature_result}')
        // Wait for the multisig transaction to be confirmed
        if ('SUCCESS' === cosignatureResult) {
            await waitForConfirmation(
                facade.hashTransaction(transaction).toString(),
                'multisig transaction');
        } else {
            console.log('Transaction rejected:', cosignatureResult);
        }

必要な連署がすべて集まると、マルチシグトランザクションは 1 つの単位として承認されます。

マルチシグトランザクションは、プロトコルの制約に違反すると拒否されます。 次の表に、よくあるエラーの原因をまとめます。

エラーメッセージ 考えられる原因
FAILURE_TRANSACTION_NOT_ALLOWED_FOR_MULTISIG マルチシグアカウント自身が送金をアナウンスしようとした。
FAILURE_MULTISIG_NOT_A_COSIGNER マルチシグトランザクションの署名者が連署人リストにない。
FAILURE_MULTISIG_NO_MATCHING_MULTISIG 連署が承認待ちのマルチシグトランザクションと一致しない、または署名者が連署人ではない。

出力⚓︎

以下は、プログラムの実行時の出力例です。

Using node http://libertalia.nemtest.net:7890
Multisig public key: D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2
Cosignatory 0 public key: AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3
Cosignatory 1 public key: 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23
Fetching current network time from /time-sync/network-time
  Network time: 357569945 s since the nemesis block
Built multisig transaction:
{
  "type": 4100,
  "version": 1,
  "network": 152,
  "timestamp": 357569945,
  "signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
  "signature": "DD065509918473534035EEFE9258E515861B250BBCF8208A77B12FE6A1AFF42F0C3D51D9A74E7A09F39C29C0CDFF860FF76F597FCE62A28433AFC8D95926A70E",
  "fee": "150000",
  "deadline": 357577145,
  "inner_transaction": {
    "type": 257,
    "version": 2,
    "network": 152,
    "timestamp": 357569945,
    "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    "fee": "50000",
    "deadline": 357577145,
    "recipient_address": "54424C58494F554F34455035595237344859585333424642474F4E5A42554850334E495332484A36",
    "amount": "1000000",
    "mosaics": []
  },
  "cosignatures": []
}
Announcing multisig transaction to /transaction/announce
  Result: SUCCESS
Fetching pending transactions from /account/unconfirmedTransactions?address=TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4
  Inner transaction hash: 24a1079e4dd5063c3a938675653a7525c640824470764cc34ab2a64c0c614c4d
Built cosignature:
{
  "type": 4098,
  "version": 1,
  "network": 152,
  "timestamp": 357569945,
  "signer_public_key": "26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23",
  "signature": "A094A18F4E63745B4939BB6979BD293856765E25A96B244AC7AB5E7334398802A824E800206DA62888DB474A1C64EBEBB285EC05EB3D08C8F4EA8C40A221A30D",
  "fee": "150000",
  "deadline": 357577145,
  "other_transaction_hash": "24A1079E4DD5063C3A938675653A7525C640824470764CC34AB2A64C0C614C4D",
  "multisig_account_address": "54424C58494F554F34455035595237344859585333424642474F4E5A42554850334E495332484A36"
}
Announcing cosignature to /transaction/announce
  Result: SUCCESS
Waiting for multisig transaction confirmation from /transaction/get?hash=12282A1E794D5D230EC8D0093E925F1BD83BB156F460DC2775045B61A779B8AA
  Transaction status: pending
  Transaction status: pending
  ...
multisig transaction confirmed in block 719496

出力の要点は次のとおりです。

  • 2~4 行目: 関係するすべてのアカウントの公開鍵。
  • 13 行目signer_public_key): マルチシグトランザクションの署名者。 連署人 0 と一致することに注意してください。
  • 22 行目signer_public_key): 内部転送トランザクションの署名者。 マルチシグアカウントと一致することに注意してください。
  • 35 行目Inner transaction hash): ネットワークから取得した承認待ちの内部トランザクションのハッシュ。
  • 42 行目signer_public_key): 連署の署名者。 連署人 1 と一致することに注意してください。
  • 46 行目other_transaction_hash): 連署が参照する内部トランザクションのハッシュ。
  • 51 行目: ネットワーク上でマルチシグトランザクションを一意に識別するハッシュ。

出力に表示されたマルチシグトランザクションのハッシュを使って、NEM テストネットエクスプローラー で承認済みトランザクションを検索できます。

まとめ⚓︎

このチュートリアルは、送信元アカウントとして マルチシグアカウント を使う点を除けば、XEM を送信する チュートリアルと機能的に同じです。

具体的には、次の方法を説明しました。

手順 関連ドキュメント
送金をマルチシグトランザクションでラップする MultisigTransactionV1
マルチシグトランザクションに署名する
承認待ちのトランザクションを見つける /account/unconfirmedTransactions GET
承認待ちのマルチシグトランザクションに連署する CosignatureV1