コンテンツにスキップ

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

中級

このチュートリアルでは、転送トランザクション チュートリアルと同様に、アカウント から自身へ 1 XYM を転送します。

ただし、このケースでは送信元が マルチシグアカウント(「マルチシグ」とも呼ばれます)であるため、アカウント単体でトランザクションを開始したり署名したりすることはできません。代わりに、連署者アカウントのいずれかがトランザクションを作成し、代理で署名を行います。

このチュートリアルでは、マルチシグアカウントの設定 チュートリアルで作成したマルチシグ構成を使用し、連署者 0 がトランザクションの開始と署名を行います。

Multisignature Treeマルチシグアカウントマルチシグアカウント連署者 0連署者 0連署者 0->マルチシグアカウント連署者 1連署者 1連署者 1->マルチシグアカウント

前提条件⚓︎

開始する前に、以下を確認してください。

さらに、トランザクションがどのようにアナウンスされ承認されるかを理解するために 転送トランザクション チュートリアルを、アグリゲートトランザクション の仕組みを理解するために コンプリートアグリゲートトランザクション のチュートリアルを復習しておいてください。

完全なコード⚓︎

このチュートリアルの完全なコード一覧を以下に示します。 詳細な手順ごとの説明は次のセクションで行います。

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id

NODE_URL = os.getenv('NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    print(f'Announcing {label} to /transactions')
    request = urllib.request.Request(
        f'{NODE_URL}/transactions',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as announce_response:
        print(f'  Response: {announce_response.read().decode()}')


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise RuntimeError(
                        f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(f'{label} not confirmed after 60 seconds')


MULTISIG_PRIVATE_KEY = os.getenv(
    'MULTISIG_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000001')
multisig_key_pair = SymbolFacade.KeyPair(
    PrivateKey(MULTISIG_PRIVATE_KEY))
print(f'Multisig public key: {multisig_key_pair.public_key}')
COSIGNATORY0_PRIVATE_KEY = os.getenv(
    'COSIGNATORY0_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000002')
cosignatory_key_pair = SymbolFacade.KeyPair(
    PrivateKey(COSIGNATORY0_PRIVATE_KEY))
print(f'Cosignatory public key: {cosignatory_key_pair.public_key}')

facade = SymbolFacade('testnet')

try:
    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')

    # Build the embedded transfer transaction
    transfer_transaction = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address':
                    facade.network.public_key_to_address(
                        multisig_key_pair.public_key),
                'mosaics': [{
                    'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                    'amount': 1_000_000  # 1 XYM
                }]
            },
            multisig_key_pair.public_key))

    # Build the wrapper aggregate transaction
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                [transfer_transaction]),
            'transactions': [transfer_transaction]
        },
        cosignatory_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)


    # Sign the aggregate transaction using the cosignatory's signature
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(cosignatory_key_pair, transaction))
    print('Built transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    # Announce the transaction
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_transaction(json_payload, 'transaction')

    # Wait for confirmation
    wait_for_confirmation(transaction_hash, 'transaction')

except urllib.error.URLError as e:
    print(e.reason)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    SymbolFacade,
    descriptors,
    generateMosaicAliasId,
    models
} from 'symbol-sdk/symbol';

const NODE_URL = 'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; 60 > attempt; attempt++) {
        await new Promise(resolve => { setTimeout(resolve, 1000); });
        const response = await fetch(
            `${NODE_URL}/transactionStatus/${transactionHash}`);
        if (!response.ok) {
            if (404 === response.status) {
                console.log('  Transaction status: unknown');
                continue;
            }
            throw new Error(`HTTP ${response.status}`);
        }
        const status = await response.json();
        console.log('  Transaction status:', status.group);
        if ('confirmed' === status.group) {
            console.log(`${label} confirmed in`, attempt, 'seconds');
            return;
        }
        if ('failed' === status.group)
            throw new Error(`${label} failed: ${status.code}`);
    }
    throw new Error(`${label} not confirmed after 60 seconds`);
}


const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000001');
const multisigKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(MULTISIG_PRIVATE_KEY));
console.log(`Multisig public key: ${multisigKeyPair.publicKey}`);
const COSIGNATORY0_PRIVATE_KEY = process.env.COSIGNATORY0_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000002';
const cosignatoryKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(COSIGNATORY0_PRIVATE_KEY));
console.log(`Cosignatory public key: ${cosignatoryKeyPair.publicKey}`);

const facade = new SymbolFacade('testnet');

try {
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);

    // Build the embedded transfer transaction
    const transferTransaction =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                facade.network.publicKeyToAddress(
                    multisigKeyPair.publicKey),
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        generateMosaicAliasId('symbol.xym'),
                        new models.Amount(1_000_000n)) // 1 XYM
                ],
                undefined),
            multisigKeyPair.publicKey);

    // Build the wrapper aggregate transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions([transferTransaction]),
            [transferTransaction],
            undefined),
        cosignatoryKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);

    // Sign the aggregate transaction using the cosignatory's signature
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(cosignatoryKeyPair, transaction));
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true });

    // Announce the transaction
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await announceTransaction(jsonPayload, 'transaction');

    // Wait for confirmation
    await waitForConfirmation(transactionHash, 'transaction');
} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

Download source

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.IdGenerator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;

public final class SignMultisig {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private final String nodeUrl = "https://reference.symboltest.net:3001";

    private final SymbolFacade facade = new SymbolFacade("testnet");

    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            final String statusPath =
                "/transactionStatus/" + transactionHash;
            final HttpRequest statusRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> statusResponse = HTTP_CLIENT
                .send(statusRequest, BodyHandlers.ofString());
            if (404 == statusResponse.statusCode()) {
                System.out.println("  Transaction status: unknown");
                continue;
            }
            if (2 != statusResponse.statusCode() / 100)
                throw new IOException(
                    "HTTP " + statusResponse.statusCode());

            final JsonNode status =
                JSON_MAPPER.readTree(statusResponse.body());
            final String group = status.get("group").asText();
            System.out.printf("  Transaction status: %s%n", group);
            if ("confirmed".equals(group)) {
                System.out.printf("%s confirmed in %d seconds%n",
                    label, attempt);
                return;
            }
            if ("failed".equals(group))
                throw new IOException(String.format("%s failed: %s",
                    label, status.get("code").asText()));
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }

    public static void main(final String[] args) {
        try {
            new SignMultisig().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

    private void run() throws IOException, InterruptedException {
        System.out.printf("Using node %s%n", nodeUrl);

        final String multisigPrivateKey = System.getenv().getOrDefault(
            "MULTISIG_PRIVATE_KEY", "%064X".formatted(1));

        final KeyPair multisigKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(multisigPrivateKey));
        System.out.printf("Multisig public key: %s%n",
            multisigKeyPair.getPublicKey());
        final String cosignatory0PrivateKey = System.getenv().getOrDefault(
            "COSIGNATORY0_PRIVATE_KEY", "%064X".formatted(2));
        final KeyPair cosignatoryKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(cosignatory0PrivateKey));
        System.out.printf("Cosignatory public key: %s%n",
            cosignatoryKeyPair.getPublicKey());


        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(feeResponse.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

        // Build the embedded transfer transaction
        final EmbeddedTransaction transferTransaction =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    facade.network.publicKeyToAddress(
                        multisigKeyPair.getPublicKey()),
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(1_000_000))), // 1 XYM
                    null),
                multisigKeyPair.getPublicKey());

        // Build the wrapper aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(transferTransaction);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                cosignatoryKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

        // Sign the aggregate using the cosignatory's signature
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(
                transaction,
                facade.signTransaction(
                    cosignatoryKeyPair, transaction));
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // Announce the transaction
        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        announceTransaction(jsonPayload, "transaction");

        // Wait for confirmation
        waitForConfirmation(transactionHash, "transaction");
    }
}

Download source

コード解説⚓︎

一般的に、マルチシグアカウントの代理でトランザクションに署名するには、必要な連署を提供する アグリゲートトランザクション でラップするだけで済みます。

このチュートリアルでは、転送の起点となるマルチシグアカウントを署名者として、転送内容を含む 埋め込みトランザクション を構築します。その後、トランザクションを承認できるアカウントである連署者が署名した コンプリートアグリゲートトランザクション で、その転送トランザクションをラップします。

アカウントの設定⚓︎

MULTISIG_PRIVATE_KEY = os.getenv(
    'MULTISIG_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000001')
multisig_key_pair = SymbolFacade.KeyPair(
    PrivateKey(MULTISIG_PRIVATE_KEY))
print(f'Multisig public key: {multisig_key_pair.public_key}')
COSIGNATORY0_PRIVATE_KEY = os.getenv(
    'COSIGNATORY0_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000002')
cosignatory_key_pair = SymbolFacade.KeyPair(
    PrivateKey(COSIGNATORY0_PRIVATE_KEY))
print(f'Cosignatory public key: {cosignatory_key_pair.public_key}')
const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || (
    '0000000000000000000000000000000000000000000000000000000000000001');
const multisigKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(MULTISIG_PRIVATE_KEY));
console.log(`Multisig public key: ${multisigKeyPair.publicKey}`);
const COSIGNATORY0_PRIVATE_KEY = process.env.COSIGNATORY0_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000002';
const cosignatoryKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(COSIGNATORY0_PRIVATE_KEY));
console.log(`Cosignatory public key: ${cosignatoryKeyPair.publicKey}`);
        final String multisigPrivateKey = System.getenv().getOrDefault(
            "MULTISIG_PRIVATE_KEY", "%064X".formatted(1));

        final KeyPair multisigKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(multisigPrivateKey));
        System.out.printf("Multisig public key: %s%n",
            multisigKeyPair.getPublicKey());
        final String cosignatory0PrivateKey = System.getenv().getOrDefault(
            "COSIGNATORY0_PRIVATE_KEY", "%064X".formatted(2));
        final KeyPair cosignatoryKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(cosignatory0PrivateKey));
        System.out.printf("Cosignatory public key: %s%n",
            cosignatoryKeyPair.getPublicKey());

このチュートリアルでは2つの個別のアカウントが必要です。 それらの 秘密鍵 は環境変数を通じて提供できます。設定されていない場合は、デフォルト値が使用されます。

環境変数 デフォルト値 用途
MULTISIG_PRIVATE_KEY 0000..0001 マルチシグアカウント
COSIGNATORY0_PRIVATE_KEY 0000..0002 連署者アカウント

各秘密鍵は64文字の16進数文字列です。

連署者アカウントは、トランザクション手数料を支払うのに十分な資金を保有している必要があります。デフォルト値を使用する場合、これらのアカウントにはすでに資金が供給されている可能性があります。

上記のスニペットは、後で使用するために各アカウントの キーペア を派生させて保存します。

    # Fetch recommended fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        response_json = json.loads(response.read().decode())
        median_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
        // Fetch recommended fees
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest feeRequest = HttpRequest.newBuilder(
            URI.create(nodeUrl + feePath)).GET().build();
        final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
            feeRequest, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(feeResponse.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

転送トランザクション チュートリアルで説明されているプロセスに従い、推奨手数料を /network/fees/transaction GET から取得します。

トランザクションの構築⚓︎

    # Build the embedded transfer transaction
    transfer_transaction = (
        facade.create_embedded_transaction_from_descriptor(
            {
                'type': 'transfer_transaction_v1',
                'recipient_address':
                    facade.network.public_key_to_address(
                        multisig_key_pair.public_key),
                'mosaics': [{
                    'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                    'amount': 1_000_000  # 1 XYM
                }]
            },
            multisig_key_pair.public_key))
    // Build the embedded transfer transaction
    const transferTransaction =
        facade.createEmbeddedTransactionFromTypedDescriptor(
            new descriptors.TransferTransactionV1Descriptor(
                facade.network.publicKeyToAddress(
                    multisigKeyPair.publicKey),
                [
                    new descriptors.UnresolvedMosaicDescriptor(
                        generateMosaicAliasId('symbol.xym'),
                        new models.Amount(1_000_000n)) // 1 XYM
                ],
                undefined),
            multisigKeyPair.publicKey);
        // Build the embedded transfer transaction
        final EmbeddedTransaction transferTransaction =
            facade.createEmbeddedTransactionFromTypedDescriptor(
                new TransferTransactionV1Descriptor(
                    facade.network.publicKeyToAddress(
                        multisigKeyPair.getPublicKey()),
                    List.of(new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(1_000_000))), // 1 XYM
                    null),
                multisigKeyPair.getPublicKey());

埋め込まれた 転送トランザクション は、トランザクションのディスクリプタと署名者の公開鍵から作成します。 署名者の公開鍵は、資金の転送元となるアカウント、つまりマルチシグアカウントの 公開鍵 です。 ディスクリプタには以下のフィールドが含まれます。

  • recipient_address: この例では、資金は送信元に戻されるため、受信者もマルチシグアカウントになります。

  • mosaics: 転送トランザクション チュートリアルで説明されているように、1 XYM に相当する symbol.xym モザイクの 1,000,000 絶対単位。

埋め込みトランザクションは、内部トランザクションが1つだけの場合でも、アグリゲートトランザクションにラップされます。

    # Build the wrapper aggregate transaction
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'aggregate_complete_transaction_v3',
            'transactions_hash': facade.hash_embedded_transactions(
                [transfer_transaction]),
            'transactions': [transfer_transaction]
        },
        cosignatory_key_pair.public_key,
        fee_multiplier,
        2 * 60 * 60)
    // Build the wrapper aggregate transaction
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.AggregateCompleteTransactionV3Descriptor(
            facade.static.hashEmbeddedTransactions([transferTransaction]),
            [transferTransaction],
            undefined),
        cosignatoryKeyPair.publicKey,
        feeMultiplier,
        2 * 60 * 60);
        // Build the wrapper aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions =
            List.of(transferTransaction);
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new AggregateCompleteTransactionV3Descriptor(
                    SymbolFacade.hashEmbeddedTransactions(
                        embeddedTransactions),
                    embeddedTransactions,
                    null),
                cosignatoryKeyPair.getPublicKey(),
                feeMultiplier,
                2 * 60 * 60);

ディスクリプタには埋め込みトランザクションが含まれます。 アグリゲートは、トランザクションを承認し、その手数料を支払う連署者の公開鍵を署名者として作成します。

  • transactions: 埋め込みトランザクションのリスト。この例では1つだけですが、いくつでも含めることができます。

簡単にするため、このチュートリアルでは コンプリートアグリゲートトランザクション を使用します。 詳細については、コンプリート および ボンデッド アグリゲートトランザクションのチュートリアルを参照してください。

最後に、連署者によってアグリゲートトランザクションに署名が行われます。

    # Sign the aggregate transaction using the cosignatory's signature
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(cosignatory_key_pair, transaction))
    print('Built transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Sign the aggregate transaction using the cosignatory's signature
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(cosignatoryKeyPair, transaction));
    console.log('Built transaction:');
    console.dir(transaction.toJson(), { colors: true });
        // Sign the aggregate using the cosignatory's signature
        final String jsonPayload = SymbolTransactionFactory
            .attachSignature(
                transaction,
                facade.signTransaction(
                    cosignatoryKeyPair, transaction));
        System.out.println("Built transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

複数の連署者

他のマルチシグ構成では、より多くの署名が必要になる場合があります。その場合、 ではなく を使用して署名を付加します。

例については マルチシグアカウントの設定 チュートリアルを参照してください。

アグリゲートトランザクションの送信⚓︎

最後のステップは、転送トランザクション チュートリアルで説明されているように、トランザクションをアナウンスして承認を待つことです。

    # Announce the transaction
    transaction_hash = facade.hash_transaction(transaction)
    print(f'Transaction hash: {transaction_hash}')
    announce_transaction(json_payload, 'transaction')

    # Wait for confirmation
    wait_for_confirmation(transaction_hash, 'transaction')
    // Announce the transaction
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log('Transaction hash:', transactionHash);
    await announceTransaction(jsonPayload, 'transaction');

    // Wait for confirmation
    await waitForConfirmation(transactionHash, 'transaction');
        // Announce the transaction
        final String transactionHash =
            facade.hashTransaction(transaction).toString();
        System.out.printf("Transaction hash: %s%n", transactionHash);
        announceTransaction(jsonPayload, "transaction");

        // Wait for confirmation
        waitForConfirmation(transactionHash, "transaction");

プロトコル制約に違反した場合、トランザクションは拒否されます。以下の表は、最も一般的なエラーの原因をまとめたものです。

エラーメッセージ 考えられる原因
Multisig Operation Prohibited By Account マルチシグアカウント自体がアグリゲートトランザクションに署名しようとした。
Aggregate Ineligible Cosignatories 署名者が連署者リストに含まれていない。
Consumer Batch Signature Not Verifiable アグリゲートトランザクションに付加された署名が、その signer_public_key と一致しない。

出力⚓︎

以下に示す出力は、プログラムの典型的な実行結果に対応しています。

Using node https://reference.symboltest.net:3001
Multisig public key: 4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29
Cosignatory public key: 7422B9887598068E32C4448A949ADB290D0F4E35B9E01B0EE5F1A1E600FE2674
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built transaction:
{
  "signature": "A533F9B281174AE537944B25752DB75FFC277CBC3958347E741E53CA4A1D02EBA9C4B6F993DF05A250F07C270E2CF6C21DA6344FE31AE701390BA5AD7BC62F0C",
  "signer_public_key": "7422B9887598068E32C4448A949ADB290D0F4E35B9E01B0EE5F1A1E600FE2674",
  "version": 3,
  "network": 152,
  "type": 16705,
  "fee": "26400",
  "deadline": "102879850165",
  "transactions_hash": "8E64E490EB8B7887CAE6BE6846F67ADEEAE0AF3525CF98D84044ADE9F6BA488F",
  "transactions": [
    {
      "signer_public_key": "4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29",
      "version": 1,
      "network": 152,
      "type": 16724,
      "recipient_address": "987D075454716222F609929E883174AD8C996D5828C938BC",
      "mosaics": [
        {
          "mosaic_id": "16666583871264174062",
          "amount": "1000000"
        }
      ],
      "message": ""
    }
  ],
  "cosignatures": []
}
Transaction hash: AB9FB75C150AD471AC73A6CF278D82D122E2583CF02B3B9067274608A7D334E4
Announcing transaction to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for transaction confirmation...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  ...
  Transaction status: confirmed
transaction confirmed in 9 seconds

出力の主なポイント:

  • 2-3行目: 関与するすべてのアカウントの公開鍵。
  • 9行目 (signer_public_key): アグリゲートトランザクションの署名者。連署者アカウントと一致していることに注目してください。
  • 18行目 (signer_public_key): 埋め込まれた転送トランザクションの署名者。マルチシグアカウントと一致していることに注目してください。
  • 22行目 (recipient_address): エンコードされたマルチシグアカウントの アドレス

出力に示されているトランザクションハッシュを使用して、Symbol Testnet Explorer でトランザクションを検索できます。

結論⚓︎

このチュートリアルは機能的には 転送トランザクション チュートリアルと同じですが、送信元アカウントとして マルチシグアカウント を使用しています。

特に、このチュートリアルでは以下の方法を説明しました。

ステップ 関連ドキュメント
転送トランザクションを埋め込みトランザクションにラップする , TransferTransactionV1
適切な場所に署名を付加する