コンテンツにスキップ

マルチシグアカウントを設定する⚓︎

上級

マルチシグアカウント は マルチシグ とも呼ばれ、そのアカウントだけではトランザクションを開始することはできません。 代わりに、連署人 アカウントがマルチシグアカウントに代わってトランザクションを作成し、署名します。

このチュートリアルでは、通常のアカウントを 2 人の連署人のうち、どちらか一方の承認を必要とするマルチシグアカウントに変換する方法を説明します。 アカウントがすでにマルチシグの場合は、連署人を削除して通常のアカウントに戻す方法を説明します。

このチュートリアルで使用するマルチシグ構成は以下の通りです。

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

前提条件⚓︎

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

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

完全なコード⚓︎

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

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.NemFacade import NemFacade
from symbolchain.nc import Amount
from symbolchain.nem.FeeCalculator import calculate_transaction_fee

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

facade = NemFacade('testnet')

KEY_TEMPLATE = '0' * 63 + '{}'

# Set up the keys for the multisig account and its two cosignatories
MULTISIG_PRIVATE_KEY = os.getenv(
    'MULTISIG_PRIVATE_KEY', KEY_TEMPLATE.format(1))
multisig_key_pair = NemFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY))
multisig_address = facade.network.public_key_to_address(
    multisig_key_pair.public_key)
print(f'Multisig address: {multisig_address} '
    f'(public key {multisig_key_pair.public_key})')

cosignatory_key_pairs = []
for i in range(2):
    COSIGNATORY_PRIVATE_KEY = os.getenv(
        f'COSIGNATORY{i}_PRIVATE_KEY', KEY_TEMPLATE.format(i + 2))
    key_pair = NemFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))
    cosignatory_key_pairs.append(key_pair)
    addr = facade.network.public_key_to_address(key_pair.public_key)
    print(f'Cosignatory {i} address: '
        f'{addr} (public key {key_pair.public_key})')


# 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.')


# Returns the cosignatory addresses of the provided multisig
# account, or an empty list if the account is not multisig
def get_multisig_cosignatories(address):
    account_path = f'/account/get?address={address}'
    print(f'Getting cosignatories from {account_path}')
    url = f'{NODE_URL}{account_path}'
    with urllib.request.urlopen(url) as account_response:
        account_info = json.loads(account_response.read().decode())
        found_cosignatories = [
            cosignatory['address']
            for cosignatory in account_info['meta']['cosignatories']
        ]
        if not found_cosignatories:
            print('  Response: No cosignatories')
            return []
        print(f'  Response: {found_cosignatories}')
        return found_cosignatories



# Returns a transaction that turns a regular account into a multisig
def multisig_enable_transaction(approval_delta):
    # Create a multisig account modification transaction
    # that adds the cosignatories
    modifications = [
        {'modification': {
            'modification_type': 'add_cosignatory',
            'cosignatory_public_key': key_pair.public_key
        }}
        for key_pair in cosignatory_key_pairs
    ]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'multisig_account_modification_transaction_v2',
            # Change of the number of cosignatures
            # required to approve transactions
            'min_approval_delta': approval_delta,
            'modifications': modifications
        },
        # This is the account that will be turned into a multisig
        multisig_key_pair.public_key,
        0,
        2 * 60 * 60)

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    print('Enabling the multisig with the modification transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    # Sign the transaction with the multisig's key
    signature = facade.sign_transaction(multisig_key_pair, transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction



# Returns a transaction that removes one cosignatory from the multisig
def multisig_removal_transaction(removed_key_pair, approval_delta):
    # Create a multisig account modification transaction
    # that removes a single cosignatory
    inner_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'multisig_account_modification_transaction_v2',
            # Change of the number of cosignatures
            # required to approve transactions
            'min_approval_delta': approval_delta,
            'modifications': [
                {'modification': {
                    'modification_type': 'delete_cosignatory',
                    'cosignatory_public_key': removed_key_pair.public_key
                }}
            ]
        },
        # This is the multisig account that will be modified
        multisig_key_pair.public_key,
        0,
        2 * 60 * 60)

    # Wrap the modification in a multisig transaction
    inner_fee = calculate_transaction_fee(inner_transaction)
    inner_transaction.fee = Amount(inner_fee)
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'multisig_transaction_v1',
            'inner_transaction':
                facade.transaction_factory.to_non_verifiable_transaction(
                    inner_transaction)
        },
        # This is the cosignatory that initiates the removal
        cosignatory_key_pairs[0].public_key,
        0,
        2 * 60 * 60)

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {(inner_fee + fee) / 1_000_000} XEM')
    print('Disabling the multisig with the multisig transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    # Sign the transaction with the cosignatory's key
    signature = facade.sign_transaction(
        cosignatory_key_pairs[0], transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction


try:
    # Get current state of the multisig account and decide which
    # operation to perform
    cosignatories = get_multisig_cosignatories(multisig_address)
    if len(cosignatories) == 0:
        # Enable the multisig
        transactions = [multisig_enable_transaction(1)]
    else:
        # Disable the multisig
        transactions = [
            multisig_removal_transaction(cosignatory_key_pairs[1], 0),
            multisig_removal_transaction(cosignatory_key_pairs[0], -1)
        ]

    # Announce each transaction and wait for confirmation
    for signed_transaction in transactions:
        transaction_hash = facade.hash_transaction(signed_transaction)
        print(f'Built transaction with hash: {transaction_hash}')
        json_payload = facade.transaction_factory.to_json(
            signed_transaction)
        announce_result = announce_transaction(
            json_payload, 'transaction')
        if 'SUCCESS' != announce_result:
            print('Transaction rejected')
            break
        wait_for_confirmation(transaction_hash, 'transaction')

except Exception as error:
    print(error)

Download source

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

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

const facade = new NemFacade('testnet');

const KEY_PREFIX = '0'.repeat(63);

// Set up the keys for the multisig account and its two cosignatories
const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || (
    `${KEY_PREFIX}1`);
const multisigKeyPair = new NemFacade.KeyPair(
    new PrivateKey(MULTISIG_PRIVATE_KEY));
const multisigAddress = facade.network.publicKeyToAddress(
    multisigKeyPair.publicKey);
console.log(`Multisig address: ${multisigAddress}`,
    `(public key ${multisigKeyPair.publicKey})`);

const cosignatoryKeyPairs = [];
for (let i = 0; 2 > i; i++) {
    const COSIGNATORY_PRIVATE_KEY =
        process.env[`COSIGNATORY${i}_PRIVATE_KEY`] || (
            KEY_PREFIX + String(i + 2));
    const keyPair = new NemFacade.KeyPair(
        new PrivateKey(COSIGNATORY_PRIVATE_KEY));
    cosignatoryKeyPairs.push(keyPair);
    const addr = facade.network.publicKeyToAddress(keyPair.publicKey);
    console.log(`Cosignatory ${i} address: ${addr}`,
        `(public key ${keyPair.publicKey})`);
}

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    const announcePath = '/transaction/announce';
    console.log(`Announcing ${label} to ${announcePath}`);
    const response = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    if (!response.ok)
        throw new Error(`HTTP ${response.status}`);
    const result = await response.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) {
            console.log('  Transaction status: pending');
            await new Promise(resolve => { setTimeout(resolve, 1000); });
        } else {
            const confirmed = await response.json();
            console.log(`${label} confirmed in block`,
                confirmed.meta.height);
            isConfirmed = true;
            break;
        }
    }
    if (!isConfirmed)
        console.warn(`${label} confirmation took too long.`);
}

// Returns the cosignatory addresses of the provided multisig
// account, or an empty list if the account is not multisig
async function getMultisigCosignatories(address) {
    const accountPath = `/account/get?address=${address}`;
    console.log(`Getting cosignatories from ${accountPath}`);
    const response = await fetch(`${NODE_URL}${accountPath}`);
    const accountInfo = await response.json();
    const foundCosignatories = accountInfo.meta.cosignatories
        .map(cosignatory => cosignatory.address);
    if (0 === foundCosignatories.length) {
        console.log('  Response: No cosignatories');
        return [];
    }
    console.log('  Response:', JSON.stringify(foundCosignatories));
    return foundCosignatories;
}


// Returns a transaction that turns a regular account into a multisig
function multisigEnableTransaction(approvalDelta) {
    // Create a multisig account modification transaction
    // that adds the cosignatories
    const modifications = cosignatoryKeyPairs.map(keyPair =>
        new descriptors.SizePrefixedMultisigAccountModificationDescriptor(
            new descriptors.MultisigAccountModificationDescriptor(
                models.MultisigAccountModificationType.ADD_COSIGNATORY,
                keyPair.publicKey)));
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MultisigAccountModificationTransactionV2Descriptor(
        // Change of the number of cosignatures
        // required to approve transactions
            approvalDelta,
            modifications),
        // This is the account that will be turned into a multisig
        multisigKeyPair.publicKey,
        0n,
        2 * 60 * 60);

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);
    console.log(
        'Enabling the multisig with the modification transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign the transaction with the multisig's key
    const signature = facade.signTransaction(
        multisigKeyPair, transaction);
    facade.transactionFactory.static.attachSignature(
        transaction, signature);
    return transaction;
}


// Returns a transaction that removes one cosignatory from the multisig
function multisigRemovalTransaction(removedKeyPair, approvalDelta) {
    // Create a multisig account modification transaction
    // that removes a single cosignatory
    const innerTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MultisigAccountModificationTransactionV2Descriptor(
        // Change of the number of cosignatures
        // required to approve transactions
            approvalDelta,
            [new descriptors.SizePrefixedMultisigAccountModificationDescriptor(
                new descriptors.MultisigAccountModificationDescriptor(
                    models.MultisigAccountModificationType.DELETE_COSIGNATORY,
                    removedKeyPair.publicKey))]),
        // This is the multisig account that will be modified
        multisigKeyPair.publicKey,
        0n,
        2 * 60 * 60);

    // Wrap the modification in a multisig transaction
    const innerFee = calculateTransactionFee(innerTransaction);
    innerTransaction.fee = new models.Amount(innerFee);
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MultisigTransactionV1Descriptor(
            facade.transactionFactory.static.toNonVerifiableTransaction(
                innerTransaction)),
        // This is the cosignatory that initiates the removal
        cosignatoryKeyPairs[0].publicKey,
        0n,
        2 * 60 * 60);

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log('  Transaction fee:',
        `${Number(innerFee + fee) / 1_000_000} XEM`);
    console.log(
        'Disabling the multisig with the multisig transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign the transaction with the cosignatory's key
    const signature = facade.signTransaction(
        cosignatoryKeyPairs[0], transaction);
    facade.transactionFactory.static
        .attachSignature(transaction, signature);
    return transaction;
}

try {
    // Get current state of the multisig account and decide
    // which operation to perform
    const cosignatories = await getMultisigCosignatories(multisigAddress);
    let transactions;
    if (0 === cosignatories.length) {
        // Enable the multisig
        transactions = [multisigEnableTransaction(1)];
    } else {
        // Disable the multisig
        transactions = [
            multisigRemovalTransaction(cosignatoryKeyPairs[1], 0),
            multisigRemovalTransaction(cosignatoryKeyPairs[0], -1)
        ];
    }

    // Announce each transaction and wait for confirmation
    for (const signedTransaction of transactions) {
        const transactionHash = facade.hashTransaction(signedTransaction)
            .toString();
        console.log('Built transaction with hash:', transactionHash);
        const jsonPayload = facade.transactionFactory.static
            .toJson(signedTransaction);
        const result = await announceTransaction(
            jsonPayload, 'transaction');
        if ('SUCCESS' !== result) {
            console.log('Transaction rejected');
            break;
        }
        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.3

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.ArrayList;
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.NemFacade;
import org.symbol.sdk.nem.Address;
import org.symbol.sdk.nem.FeeCalculator;
import org.symbol.sdk.nem.KeyPair;
import org.symbol.sdk.nem.NemTransactionFactory;
import org.symbol.sdk.nem.descriptors.*;
import org.symbol.sdk.nem.models.Amount;
import org.symbol.sdk.nem.models.MultisigAccountModificationType;
import org.symbol.sdk.nem.models.Transaction;

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

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

    private final String nodeUrl = System.getenv().getOrDefault(
        "NODE_URL", "http://libertalia.nemtest.net:7890");

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

    // Helper method to announce a transaction
    private String announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        final String announcePath = "/transaction/announce";
        System.out.printf("Announcing %s to %s%n", label, announcePath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + announcePath))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        if (2 != response.statusCode() / 100)
            throw new IOException("HTTP " + response.statusCode());
        final String result = JSON_MAPPER.readTree(response.body())
            .get("message").asText();
        System.out.printf("  Result: %s%n", result);
        return result;
    }
    // Helper method to wait for transaction confirmation
    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        final String statusPath = String.format(
            "/transaction/get?hash=%s", transactionHash);
        System.out.printf("Waiting for %s confirmation from %s%n",
            label, statusPath);
        boolean isConfirmed = false;
        for (int attempt = 1; 120 >= attempt; ++attempt) {
            final HttpRequest request = HttpRequest.newBuilder(
                URI.create(nodeUrl + statusPath)).GET().build();
            final HttpResponse<String> response = HTTP_CLIENT.send(
                request, BodyHandlers.ofString());
            if (2 != response.statusCode() / 100) {
                System.out.println("  Transaction status: pending");
                Thread.sleep(1000);
            } else {
                final JsonNode confirmed = JSON_MAPPER.readTree(
                    response.body());
                System.out.printf("%s confirmed in block %s%n", label,
                    confirmed.get("meta").get("height").asText());
                isConfirmed = true;
                break;
            }
        }
        if (!isConfirmed)
        System.out.printf("%s confirmation took too long.%n", label);
    }

    public static void main(final String[] args) {
        try {
            new ConfigureMultisig().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 keyPrefix = "0".repeat(63);
        final String multisigPrivateKey = System.getenv().getOrDefault(
            "MULTISIG_PRIVATE_KEY", keyPrefix + "1");
        final KeyPair multisigKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(multisigPrivateKey));
        final Address multisigAddress = facade.network.publicKeyToAddress(
            multisigKeyPair.getPublicKey());
        System.out.printf("Multisig address: %s (public key %s)%n",
            multisigAddress, multisigKeyPair.getPublicKey());
        final List<KeyPair> cosignatoryKeyPairs = new ArrayList<>();
        for (int i = 0; 2 > i; ++i) {
            final String privateKey = System.getenv().getOrDefault(
                String.format("COSIGNATORY%d_PRIVATE_KEY", i),
                keyPrefix + (i + 2));
            final KeyPair keyPair = new KeyPair(
                new CryptoTypes.PrivateKey(privateKey));
            cosignatoryKeyPairs.add(keyPair);
            final Address address = facade.network.publicKeyToAddress(
                keyPair.getPublicKey());
            System.out.printf(
                "Cosignatory %d address: %s (public key %s)%n",
                i, address, keyPair.getPublicKey());
        }

        // Get the current state of the multisig account
        final List<String> cosignatories = getMultisigCosignatories(
            multisigAddress);
        // Decide which operation to perform
        final List<Transaction> transactions;
        if (cosignatories.isEmpty()) {
            transactions = List.of(multisigEnableTransaction(
                multisigKeyPair, cosignatoryKeyPairs, 1));
        } else {
            transactions = List.of(
                multisigRemovalTransaction(multisigKeyPair,
                    cosignatoryKeyPairs, cosignatoryKeyPairs.get(1), 0),
                multisigRemovalTransaction(multisigKeyPair,
                    cosignatoryKeyPairs, cosignatoryKeyPairs.get(0), -1));
        }

        // Announce each transaction and wait for confirmation
        for (final Transaction transaction : transactions) {
            final String hash = facade.hashTransaction(transaction)
                .toString();
            System.out.printf("Built transaction with hash: %s%n", hash);
            final String result = announceTransaction(
                NemTransactionFactory.toJson(transaction), "transaction");
            if (!"SUCCESS".equals(result)) {
                System.out.println("Transaction rejected");
                break;
            }
            waitForConfirmation(hash, "transaction");
        }

    }

    // Returns the cosignatory addresses of the provided multisig
    // account, or an empty list if the account is not multisig
    private List<String> getMultisigCosignatories(
        final Address address
    ) throws IOException, InterruptedException {
        final String path = "/account/get?address=" + address;
        System.out.printf("Getting cosignatories from %s%n", path);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final JsonNode found = JSON_MAPPER.readTree(HTTP_CLIENT.send(
            request, BodyHandlers.ofString()).body())
            .get("meta").get("cosignatories");
        final List<String> result = new ArrayList<>();
        for (final JsonNode cosignatory : found)
            result.add(cosignatory.get("address").asText());
        if (result.isEmpty())
            System.out.println("  Response: No cosignatories");
        else
            System.out.printf("  Response: %s%n", result);
        return result;
    }



    // Returns a transaction that turns a regular account into a multisig
    private Transaction multisigEnableTransaction(
        final KeyPair multisigKeyPair,
        final List<KeyPair> cosignatoryKeyPairs,
        final int approvalDelta
    ) throws IOException {
        final List<SizePrefixedMultisigAccountModificationDescriptor>
            modifications = new ArrayList<>();
        for (final KeyPair keyPair : cosignatoryKeyPairs)
            modifications.add(
                new SizePrefixedMultisigAccountModificationDescriptor(
                    new MultisigAccountModificationDescriptor(
                        MultisigAccountModificationType.ADD_COSIGNATORY,
                        keyPair.getPublicKey())));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new MultisigAccountModificationTransactionV2Descriptor(
                    approvalDelta, modifications),
                multisigKeyPair.getPublicKey(), 0, 2 * 60 * 60);

        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction);
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);
        System.out.println(
            "Enabling the multisig with the modification transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // Sign the transaction with the multisig's key
        NemTransactionFactory.attachSignature(transaction,
            facade.signTransaction(multisigKeyPair, transaction));
        return transaction;
    }


    // Returns a transaction that removes one cosignatory from the multisig
    private Transaction multisigRemovalTransaction(
        final KeyPair multisigKeyPair,
        final List<KeyPair> cosignatoryKeyPairs,
        final KeyPair removedKeyPair,
        final int approvalDelta
    ) throws IOException {
        final SizePrefixedMultisigAccountModificationDescriptor removal =
            new SizePrefixedMultisigAccountModificationDescriptor(
                new MultisigAccountModificationDescriptor(
                    MultisigAccountModificationType.DELETE_COSIGNATORY,
                    removedKeyPair.getPublicKey()));
        final Transaction innerTransaction =
            facade.createTransactionFromTypedDescriptor(
                new MultisigAccountModificationTransactionV2Descriptor(
                    approvalDelta, List.of(removal)),
                multisigKeyPair.getPublicKey(), 0, 2 * 60 * 60);

        // Wrap the modification in a multisig transaction
        final long innerFee = FeeCalculator.calculateTransactionFee(
            innerTransaction);
        innerTransaction.setFee(new Amount(innerFee));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new MultisigTransactionV1Descriptor(
                    NemTransactionFactory.toNonVerifiableTransaction(
                        innerTransaction), List.of()),
                cosignatoryKeyPairs.get(0).getPublicKey(),
                0, 2 * 60 * 60);

        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction);
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) (innerFee + fee) / 1_000_000);
        System.out.println(
            "Disabling the multisig with the multisig transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        // Sign the transaction with the cosignatory's key
        NemTransactionFactory.attachSignature(transaction,
            facade.signTransaction(
                cosignatoryKeyPairs.get(0), transaction));
        return transaction;
    }
}

Download source

コードの説明⚓︎

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

次に、マルチシグアカウントに必要な キーをセットアップ し、 マルチシグアカウントの 現在の設定を判定 します。

アカウントがすでにマルチシグとして設定されているかどうかに応じて、 マルチシグを 有効化 または 無効化 するトランザクションを作成します。 最後に、トランザクションを アナウンスして承認 します。

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

KEY_TEMPLATE = '0' * 63 + '{}'

# Set up the keys for the multisig account and its two cosignatories
MULTISIG_PRIVATE_KEY = os.getenv(
    'MULTISIG_PRIVATE_KEY', KEY_TEMPLATE.format(1))
multisig_key_pair = NemFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY))
multisig_address = facade.network.public_key_to_address(
    multisig_key_pair.public_key)
print(f'Multisig address: {multisig_address} '
    f'(public key {multisig_key_pair.public_key})')

cosignatory_key_pairs = []
for i in range(2):
    COSIGNATORY_PRIVATE_KEY = os.getenv(
        f'COSIGNATORY{i}_PRIVATE_KEY', KEY_TEMPLATE.format(i + 2))
    key_pair = NemFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))
    cosignatory_key_pairs.append(key_pair)
    addr = facade.network.public_key_to_address(key_pair.public_key)
    print(f'Cosignatory {i} address: '
        f'{addr} (public key {key_pair.public_key})')
const KEY_PREFIX = '0'.repeat(63);

// Set up the keys for the multisig account and its two cosignatories
const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || (
    `${KEY_PREFIX}1`);
const multisigKeyPair = new NemFacade.KeyPair(
    new PrivateKey(MULTISIG_PRIVATE_KEY));
const multisigAddress = facade.network.publicKeyToAddress(
    multisigKeyPair.publicKey);
console.log(`Multisig address: ${multisigAddress}`,
    `(public key ${multisigKeyPair.publicKey})`);

const cosignatoryKeyPairs = [];
for (let i = 0; 2 > i; i++) {
    const COSIGNATORY_PRIVATE_KEY =
        process.env[`COSIGNATORY${i}_PRIVATE_KEY`] || (
            KEY_PREFIX + String(i + 2));
    const keyPair = new NemFacade.KeyPair(
        new PrivateKey(COSIGNATORY_PRIVATE_KEY));
    cosignatoryKeyPairs.push(keyPair);
    const addr = facade.network.publicKeyToAddress(keyPair.publicKey);
    console.log(`Cosignatory ${i} address: ${addr}`,
        `(public key ${keyPair.publicKey})`);
}
        final String keyPrefix = "0".repeat(63);
        final String multisigPrivateKey = System.getenv().getOrDefault(
            "MULTISIG_PRIVATE_KEY", keyPrefix + "1");
        final KeyPair multisigKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(multisigPrivateKey));
        final Address multisigAddress = facade.network.publicKeyToAddress(
            multisigKeyPair.getPublicKey());
        System.out.printf("Multisig address: %s (public key %s)%n",
            multisigAddress, multisigKeyPair.getPublicKey());
        final List<KeyPair> cosignatoryKeyPairs = new ArrayList<>();
        for (int i = 0; 2 > i; ++i) {
            final String privateKey = System.getenv().getOrDefault(
                String.format("COSIGNATORY%d_PRIVATE_KEY", i),
                keyPrefix + (i + 2));
            final KeyPair keyPair = new KeyPair(
                new CryptoTypes.PrivateKey(privateKey));
            cosignatoryKeyPairs.add(keyPair);
            final Address address = facade.network.publicKeyToAddress(
                keyPair.getPublicKey());
            System.out.printf(
                "Cosignatory %d address: %s (public key %s)%n",
                i, address, keyPair.getPublicKey());
        }

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

環境変数 デフォルト値 用途
MULTISIG_PRIVATE_KEY 0000..0001 マルチシグアカウント
COSIGNATORY0_PRIVATE_KEY 0000..0002 1 人目の連署人アカウント
COSIGNATORY1_PRIVATE_KEY 0000..0003 2 人目の連署人アカウント

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

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

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

マルチシグ操作を判定する⚓︎

# Returns the cosignatory addresses of the provided multisig
# account, or an empty list if the account is not multisig
def get_multisig_cosignatories(address):
    account_path = f'/account/get?address={address}'
    print(f'Getting cosignatories from {account_path}')
    url = f'{NODE_URL}{account_path}'
    with urllib.request.urlopen(url) as account_response:
        account_info = json.loads(account_response.read().decode())
        found_cosignatories = [
            cosignatory['address']
            for cosignatory in account_info['meta']['cosignatories']
        ]
        if not found_cosignatories:
            print('  Response: No cosignatories')
            return []
        print(f'  Response: {found_cosignatories}')
        return found_cosignatories
// Returns the cosignatory addresses of the provided multisig
// account, or an empty list if the account is not multisig
async function getMultisigCosignatories(address) {
    const accountPath = `/account/get?address=${address}`;
    console.log(`Getting cosignatories from ${accountPath}`);
    const response = await fetch(`${NODE_URL}${accountPath}`);
    const accountInfo = await response.json();
    const foundCosignatories = accountInfo.meta.cosignatories
        .map(cosignatory => cosignatory.address);
    if (0 === foundCosignatories.length) {
        console.log('  Response: No cosignatories');
        return [];
    }
    console.log('  Response:', JSON.stringify(foundCosignatories));
    return foundCosignatories;
}
    // Returns the cosignatory addresses of the provided multisig
    // account, or an empty list if the account is not multisig
    private List<String> getMultisigCosignatories(
        final Address address
    ) throws IOException, InterruptedException {
        final String path = "/account/get?address=" + address;
        System.out.printf("Getting cosignatories from %s%n", path);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + path)).GET().build();
        final JsonNode found = JSON_MAPPER.readTree(HTTP_CLIENT.send(
            request, BodyHandlers.ofString()).body())
            .get("meta").get("cosignatories");
        final List<String> result = new ArrayList<>();
        for (final JsonNode cosignatory : found)
            result.add(cosignatory.get("address").asText());
        if (result.isEmpty())
            System.out.println("  Response: No cosignatories");
        else
            System.out.printf("  Response: %s%n", result);
        return result;
    }

このヘルパーは /account/get GET エンドポイントを使い、指定したアドレスの現在の連署人一覧を取得します。 空のリストが返された場合、そのアカウントはマルチシグアカウントとして設定されていません。

既存のマルチシグ設定を確認してください

分かりやすくするため、このチュートリアルでは、連署人のリストが 空でない 場合、そのアカウントはこのチュートリアルで設定されたマルチシグであると仮定します。

連署人が異なる場合など、設定が想定したものではない場合、削除トランザクションは拒否されます。

アプリケーションでは、変更を試みる前に、連署人の完全なリストと必要な署名の最小数を含む現在の設定を必ず確認してください。

    # Get current state of the multisig account and decide which
    # operation to perform
    cosignatories = get_multisig_cosignatories(multisig_address)
    if len(cosignatories) == 0:
        # Enable the multisig
        transactions = [multisig_enable_transaction(1)]
    else:
        # Disable the multisig
        transactions = [
            multisig_removal_transaction(cosignatory_key_pairs[1], 0),
            multisig_removal_transaction(cosignatory_key_pairs[0], -1)
        ]
    // Get current state of the multisig account and decide
    // which operation to perform
    const cosignatories = await getMultisigCosignatories(multisigAddress);
    let transactions;
    if (0 === cosignatories.length) {
        // Enable the multisig
        transactions = [multisigEnableTransaction(1)];
    } else {
        // Disable the multisig
        transactions = [
            multisigRemovalTransaction(cosignatoryKeyPairs[1], 0),
            multisigRemovalTransaction(cosignatoryKeyPairs[0], -1)
        ];
    }
        // Decide which operation to perform
        final List<Transaction> transactions;
        if (cosignatories.isEmpty()) {
            transactions = List.of(multisigEnableTransaction(
                multisigKeyPair, cosignatoryKeyPairs, 1));
        } else {
            transactions = List.of(
                multisigRemovalTransaction(multisigKeyPair,
                    cosignatoryKeyPairs, cosignatoryKeyPairs.get(1), 0),
                multisigRemovalTransaction(multisigKeyPair,
                    cosignatoryKeyPairs, cosignatoryKeyPairs.get(0), -1));
        }

返された連署人によって、アカウントがマルチシグとして設定されているかどうかが決まり、マルチシグを有効化または無効化するトランザクションを作成するかどうかも決まります。

トランザクションを構築する関数と、それらが使用する差分値については、次の 2 つのセクションで説明します。

マルチシグを有効化する⚓︎

# Returns a transaction that turns a regular account into a multisig
def multisig_enable_transaction(approval_delta):
    # Create a multisig account modification transaction
    # that adds the cosignatories
    modifications = [
        {'modification': {
            'modification_type': 'add_cosignatory',
            'cosignatory_public_key': key_pair.public_key
        }}
        for key_pair in cosignatory_key_pairs
    ]
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'multisig_account_modification_transaction_v2',
            # Change of the number of cosignatures
            # required to approve transactions
            'min_approval_delta': approval_delta,
            'modifications': modifications
        },
        # This is the account that will be turned into a multisig
        multisig_key_pair.public_key,
        0,
        2 * 60 * 60)
// Returns a transaction that turns a regular account into a multisig
function multisigEnableTransaction(approvalDelta) {
    // Create a multisig account modification transaction
    // that adds the cosignatories
    const modifications = cosignatoryKeyPairs.map(keyPair =>
        new descriptors.SizePrefixedMultisigAccountModificationDescriptor(
            new descriptors.MultisigAccountModificationDescriptor(
                models.MultisigAccountModificationType.ADD_COSIGNATORY,
                keyPair.publicKey)));
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MultisigAccountModificationTransactionV2Descriptor(
        // Change of the number of cosignatures
        // required to approve transactions
            approvalDelta,
            modifications),
        // This is the account that will be turned into a multisig
        multisigKeyPair.publicKey,
        0n,
        2 * 60 * 60);
    // Returns a transaction that turns a regular account into a multisig
    private Transaction multisigEnableTransaction(
        final KeyPair multisigKeyPair,
        final List<KeyPair> cosignatoryKeyPairs,
        final int approvalDelta
    ) throws IOException {
        final List<SizePrefixedMultisigAccountModificationDescriptor>
            modifications = new ArrayList<>();
        for (final KeyPair keyPair : cosignatoryKeyPairs)
            modifications.add(
                new SizePrefixedMultisigAccountModificationDescriptor(
                    new MultisigAccountModificationDescriptor(
                        MultisigAccountModificationType.ADD_COSIGNATORY,
                        keyPair.getPublicKey())));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new MultisigAccountModificationTransactionV2Descriptor(
                    approvalDelta, modifications),
                multisigKeyPair.getPublicKey(), 0, 2 * 60 * 60);

連署人の追加や削除を含むアカウントのマルチシグ設定変更は、すべて MultisigAccountModificationTransactionV2 を使って行います。

トランザクションでは、次の項目を指定します。

  • トランザクションタイプ: マルチシグ設定の変更では MultisigAccountModificationTransactionV2 を使用します。

  • : マルチシグアカウントからのトランザクションを承認するために必要な連署数の 希望値 と 現在値 の差分。

    この場合、アカウントは最初は通常のアカウントなので、必要な連署数の現在値は 0 です。 連署人の 1 人からの署名を必要とするマルチシグアカウントに変換するため、差分を 1 に設定します。

    次のセクションで示すように、現在値を 減らす 場合は差分を負の値にします。

  • : アカウントの連署人に対する変更のリスト。 各変更では、公開鍵 で識別される 1 人の連署人を追加または削除します。

    この場合、add_cosignatory 変更を 2 つ使って、セットアップ段階 で準備した連署人を追加します。

ファサードはマルチシグ設定を変更するアカウントを署名者として追加し、2 時間のデッドライン期間からタイムスタンプとデッドラインを導出します。

安全対策

プロトコルには、アカウントが無効な状態に固定されることを防ぐ安全機構が含まれています。 無効なマルチシグ設定になるトランザクションはエラーで拒否されます。 例えば、次の場合です。

  • 登録済みの連署人の数が、承認に必要な連署数に達していない
  • すでに連署人であるアカウントを追加する
  • 連署人ではないアカウントを削除する
  • 1 つのトランザクションで複数の連署人を削除する
  • マルチシグアカウントを連署人として追加する
    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    print('Enabling the multisig with the modification transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);
    console.log(
        'Enabling the multisig with the modification transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));
        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction);
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) fee / 1_000_000);
        System.out.println(
            "Enabling the multisig with the modification transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

トランザクション手数料は で計算し、トランザクションに付加します。 マルチシグアカウント変更トランザクションの固定手数料は 0.5 XEM で、手数料表 に示されています。

    # Sign the transaction with the multisig's key
    signature = facade.sign_transaction(multisig_key_pair, transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction
    // Sign the transaction with the multisig's key
    const signature = facade.signTransaction(
        multisigKeyPair, transaction);
    facade.transactionFactory.static.attachSignature(
        transaction, signature);
    return transaction;
        // Sign the transaction with the multisig's key
        NemTransactionFactory.attachSignature(transaction,
            facade.signTransaction(multisigKeyPair, transaction));
        return transaction;

最後に、トランザクションに署名します。 この場合、マルチシグに変換するアカウントの署名だけが必要です。 連署人は変換トランザクションに署名しません。

以降は連署人がトランザクションを開始します

アカウントでマルチシグを有効にすると、そのアカウント自身の署名は受け付けられなくなります。 そのアカウントから送信するトランザクション(送金や、さらにマルチシグを変更するトランザクションなど)は、次のセクションに示すように、連署人が開始して署名する必要があります。

マルチシグを無効化する⚓︎

マルチシグ設定を無効にするには、すべての連署人を削除する必要があります。 手順は有効化の場合と似ていますが、2 つの重要な違いがあります。 連署人は 1 人ずつ削除する必要があり、マルチシグアカウント自身はトランザクションに署名できません。

# Returns a transaction that removes one cosignatory from the multisig
def multisig_removal_transaction(removed_key_pair, approval_delta):
    # Create a multisig account modification transaction
    # that removes a single cosignatory
    inner_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'multisig_account_modification_transaction_v2',
            # Change of the number of cosignatures
            # required to approve transactions
            'min_approval_delta': approval_delta,
            'modifications': [
                {'modification': {
                    'modification_type': 'delete_cosignatory',
                    'cosignatory_public_key': removed_key_pair.public_key
                }}
            ]
        },
        # This is the multisig account that will be modified
        multisig_key_pair.public_key,
        0,
        2 * 60 * 60)
// Returns a transaction that removes one cosignatory from the multisig
function multisigRemovalTransaction(removedKeyPair, approvalDelta) {
    // Create a multisig account modification transaction
    // that removes a single cosignatory
    const innerTransaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MultisigAccountModificationTransactionV2Descriptor(
        // Change of the number of cosignatures
        // required to approve transactions
            approvalDelta,
            [new descriptors.SizePrefixedMultisigAccountModificationDescriptor(
                new descriptors.MultisigAccountModificationDescriptor(
                    models.MultisigAccountModificationType.DELETE_COSIGNATORY,
                    removedKeyPair.publicKey))]),
        // This is the multisig account that will be modified
        multisigKeyPair.publicKey,
        0n,
        2 * 60 * 60);
    // Returns a transaction that removes one cosignatory from the multisig
    private Transaction multisigRemovalTransaction(
        final KeyPair multisigKeyPair,
        final List<KeyPair> cosignatoryKeyPairs,
        final KeyPair removedKeyPair,
        final int approvalDelta
    ) throws IOException {
        final SizePrefixedMultisigAccountModificationDescriptor removal =
            new SizePrefixedMultisigAccountModificationDescriptor(
                new MultisigAccountModificationDescriptor(
                    MultisigAccountModificationType.DELETE_COSIGNATORY,
                    removedKeyPair.getPublicKey()));
        final Transaction innerTransaction =
            facade.createTransactionFromTypedDescriptor(
                new MultisigAccountModificationTransactionV2Descriptor(
                    approvalDelta, List.of(removal)),
                multisigKeyPair.getPublicKey(), 0, 2 * 60 * 60);

このヘルパーは、連署人を削除する MultisigAccountModificationTransactionV2 を構築します。 削除する連署人と適用する承認差分をパラメーターとして受け取ります。 設定を変更する対象がマルチシグアカウントであるため、その公開鍵を署名者としてファサードに渡します。

マルチシグ操作を判定する で示したように、このヘルパーは 2 回呼び出されます。

1 回目の呼び出しでは、 を承認差分 0 で削除します。これは、連署人が 1 人残るためです。

2 回目の呼び出しでは、残った連署人を承認差分 -1 で削除し、必要な連署数を 1 から 0 に減らします。

    # Wrap the modification in a multisig transaction
    inner_fee = calculate_transaction_fee(inner_transaction)
    inner_transaction.fee = Amount(inner_fee)
    transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'multisig_transaction_v1',
            'inner_transaction':
                facade.transaction_factory.to_non_verifiable_transaction(
                    inner_transaction)
        },
        # This is the cosignatory that initiates the removal
        cosignatory_key_pairs[0].public_key,
        0,
        2 * 60 * 60)
    // Wrap the modification in a multisig transaction
    const innerFee = calculateTransactionFee(innerTransaction);
    innerTransaction.fee = new models.Amount(innerFee);
    const transaction = facade.createTransactionFromTypedDescriptor(
        new descriptors.MultisigTransactionV1Descriptor(
            facade.transactionFactory.static.toNonVerifiableTransaction(
                innerTransaction)),
        // This is the cosignatory that initiates the removal
        cosignatoryKeyPairs[0].publicKey,
        0n,
        2 * 60 * 60);
        // Wrap the modification in a multisig transaction
        final long innerFee = FeeCalculator.calculateTransactionFee(
            innerTransaction);
        innerTransaction.setFee(new Amount(innerFee));
        final Transaction transaction =
            facade.createTransactionFromTypedDescriptor(
                new MultisigTransactionV1Descriptor(
                    NemTransactionFactory.toNonVerifiableTransaction(
                        innerTransaction), List.of()),
                cosignatoryKeyPairs.get(0).getPublicKey(),
                0, 2 * 60 * 60);

マルチシグアカウントは自分でトランザクションに署名できないため、それぞれの変更を MultisigTransactionV1 でラップします。

内部の変更トランザクションは で変換し、ラップ用のマルチシグトランザクションに埋め込めるようにします。

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {(inner_fee + fee) / 1_000_000} XEM')
    print('Disabling the multisig with the multisig transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log('  Transaction fee:',
        `${Number(innerFee + fee) / 1_000_000} XEM`);
    console.log(
        'Disabling the multisig with the multisig transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));
        // Calculate and attach the transaction fee
        final long fee = FeeCalculator.calculateTransactionFee(
            transaction);
        transaction.setFee(new Amount(fee));
        System.out.printf("  Transaction fee: %s XEM%n",
            (double) (innerFee + fee) / 1_000_000);
        System.out.println(
            "Disabling the multisig with the multisig transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

内部トランザクションとラッパーの両方に手数料がかかります。変更には 0.5 XEM、マルチシグラッパーには 0.15 XEM で、手数料表 に示されています。 どちらの手数料もマルチシグアカウントから差し引かれます。 マルチシグに代わってトランザクションを開始する連署人が手数料を支払うことはありません。

    # Sign the transaction with the cosignatory's key
    signature = facade.sign_transaction(
        cosignatory_key_pairs[0], transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction
    // Sign the transaction with the cosignatory's key
    const signature = facade.signTransaction(
        cosignatoryKeyPairs[0], transaction);
    facade.transactionFactory.static
        .attachSignature(transaction, signature);
    return transaction;
        // Sign the transaction with the cosignatory's key
        NemTransactionFactory.attachSignature(transaction,
            facade.signTransaction(
                cosignatoryKeyPairs.get(0), transaction));
        return transaction;

最後に、それぞれのマルチシグトランザクションに、それを開始し、ラッパーの署名者としてファサードに公開鍵を渡した連署人が署名します。 ここでは、2 つの削除を が開始して署名します。

このマルチシグでは必要な署名が 1 つだけなので、署名は 1 つで十分です。 より厳しい設定では、他のトランザクションと同様に、削除に追加の連署人の承認が必要になります。ただし、削除対象の連署人自身の署名は要件に数えられません。

最後に残った連署人の削除は特殊なケースです。 マルチシグアカウントの代わりに署名できるのは連署人だけなので、最後の連署人が 2 つ目のトランザクションで示すように、自身の削除に署名します。

連署人を逆の順序で削除することもできます。 違いは、各トランザクションを開始して署名する連署人だけです。

その他の設定を無効化する

削除トランザクションが拒否された場合、このチュートリアルのデフォルトとは異なる設定、例えば 2-of-2 マルチシグになっている可能性があります。

/account/get GET が返す minCosignatories フィールドで必要な連署数を確認し、必要に応じて削除トランザクションを調整してください。

例えば、2-of-2 マルチシグを無効にするには、次のようにします。

  1. Cosignatory 1 を min_approval_delta を -1 に設定して削除します。連署人が 1 人残った状態で、アカウントが 2 つの署名を必要とし続けることはできないためです。
  2. 承認されたら、min_approval_delta を -1 に設定して Cosignatory 0 を削除します。

この場合、削除対象の連署人自身の署名は必要な承認に数えられないため、Cosignatory 0 は両方の削除に署名できます。

トランザクションを送信する⚓︎

    # Announce each transaction and wait for confirmation
    for signed_transaction in transactions:
        transaction_hash = facade.hash_transaction(signed_transaction)
        print(f'Built transaction with hash: {transaction_hash}')
        json_payload = facade.transaction_factory.to_json(
            signed_transaction)
        announce_result = announce_transaction(
            json_payload, 'transaction')
        if 'SUCCESS' != announce_result:
            print('Transaction rejected')
            break
        wait_for_confirmation(transaction_hash, 'transaction')
    // Announce each transaction and wait for confirmation
    for (const signedTransaction of transactions) {
        const transactionHash = facade.hashTransaction(signedTransaction)
            .toString();
        console.log('Built transaction with hash:', transactionHash);
        const jsonPayload = facade.transactionFactory.static
            .toJson(signedTransaction);
        const result = await announceTransaction(
            jsonPayload, 'transaction');
        if ('SUCCESS' !== result) {
            console.log('Transaction rejected');
            break;
        }
        await waitForConfirmation(transactionHash, 'transaction');
    }
        // Announce each transaction and wait for confirmation
        for (final Transaction transaction : transactions) {
            final String hash = facade.hashTransaction(transaction)
                .toString();
            System.out.printf("Built transaction with hash: %s%n", hash);
            final String result = announceTransaction(
                NemTransactionFactory.toJson(transaction), "transaction");
            if (!"SUCCESS".equals(result)) {
                System.out.println("Transaction rejected");
                break;
            }
            waitForConfirmation(hash, "transaction");
        }

最後の手順では、XEM を送信する チュートリアルで説明したように、トランザクションをアナウンスして承認を待ちます。

マルチシグを無効にする場合、2 つのマルチシグトランザクションを順番にアナウンスします。 2 回目の削除は 1 回目の削除が処理された後でのみ有効になるため、コードは 1 回目のトランザクションが承認されてから 2 回目をアナウンスします。

出力⚓︎

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

Using node http://libertalia.nemtest.net:7890
Multisig address: TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 (public key D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2)
Cosignatory 0 address: TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX (public key AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3)
Cosignatory 1 address: TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4 (public key 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23)
Getting cosignatories from /account/get?address=TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6
  Response: No cosignatories
  Transaction fee: 0.5 XEM
Enabling the multisig with the modification transaction:
{
  "type": 4097,
  "version": 2,
  "network": 152,
  "timestamp": 357324103,
  "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "fee": "500000",
  "deadline": 357331303,
  "modifications": [
    {
      "modification": {
        "modification_type": 1,
        "cosignatory_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3"
      }
    },
    {
      "modification": {
        "modification_type": 1,
        "cosignatory_public_key": "26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23"
      }
    }
  ],
  "min_approval_delta": 1
}
Built transaction with hash: 4F50710F7AB5C92ED1913D0EB4EE2E29D2AA6CC9C5C6509FB1C43BFDAF124C8B
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=4F50710F7AB5C92ED1913D0EB4EE2E29D2AA6CC9C5C6509FB1C43BFDAF124C8B
  Transaction status: pending
  Transaction status: pending
  ...
transaction confirmed in block 715434

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

  • 2~4 行目: 関係するすべてのアカウントのアドレスと公開鍵。
  • 6 行目(Response: No cosignatories): 現在、連署人が設定されていない。
  • 22 行目と 28 行目(cosignatory_public_key): 追加される連署人の公開鍵。
  • 32 行目("min_approval_delta": 1): 必要な署名数が 1 つ増える。
Using node http://libertalia.nemtest.net:7890
Multisig address: TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 (public key D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2)
Cosignatory 0 address: TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX (public key AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3)
Cosignatory 1 address: TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4 (public key 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23)
Getting cosignatories from /account/get?address=TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6
  Response: ['TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4', 'TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX']
  Transaction fee: 0.65 XEM
Disabling the multisig with the multisig transaction:
{
  "type": 4100,
  "version": 1,
  "network": 152,
  "timestamp": 357324204,
  "signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "fee": "150000",
  "deadline": 357331404,
  "inner_transaction": {
    "type": 4097,
    "version": 2,
    "network": 152,
    "timestamp": 357324204,
    "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    "fee": "500000",
    "deadline": 357331404,
    "modifications": [
      {
        "modification": {
          "modification_type": 2,
          "cosignatory_public_key": "26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23"
        }
      }
    ],
    "min_approval_delta": 0
  },
  "cosignatures": []
}
  Transaction fee: 0.65 XEM
Disabling the multisig with the multisig transaction:
{
  "type": 4100,
  "version": 1,
  "network": 152,
  "timestamp": 357324204,
  "signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "fee": "150000",
  "deadline": 357331404,
  "inner_transaction": {
    "type": 4097,
    "version": 2,
    "network": 152,
    "timestamp": 357324204,
    "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    "fee": "500000",
    "deadline": 357331404,
    "modifications": [
      {
        "modification": {
          "modification_type": 2,
          "cosignatory_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3"
        }
      }
    ],
    "min_approval_delta": -1
  },
  "cosignatures": []
}
Built transaction with hash: E66D3B5D36D7711C5A3C0D99C502258345E086241DF7CCD810DFB4A0ED8CC90D
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=E66D3B5D36D7711C5A3C0D99C502258345E086241DF7CCD810DFB4A0ED8CC90D
  Transaction status: pending
  Transaction status: pending
  ...
transaction confirmed in block 715438
Built transaction with hash: 70CAE1EEF8432A834C0E4EBE3A1EB6A1774F4AE42939E05AE9F120A9AF456051
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=70CAE1EEF8432A834C0E4EBE3A1EB6A1774F4AE42939E05AE9F120A9AF456051
  Transaction status: pending
  Transaction status: pending
  ...
transaction confirmed in block 715439

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

  • 2~4 行目: 関係するすべてのアカウントのアドレスと公開鍵。
  • 6 行目(Response: [ ... ]): 既存の連署人が検出された。
  • 27~35 行目(1 つ目のマルチシグトランザクション): 必要な署名数は変わらず、既存の連署人が 1 人削除される。
  • 59~67 行目(2 つ目のマルチシグトランザクション): 必要な署名数が 1 つ減り、最後に残った連署人が削除される。

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

まとめ⚓︎

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

手順 関連ドキュメント
現在のマルチシグ設定を取得する /account/get GET
マルチシグアカウントを有効化する MultisigAccountModificationTransactionV2
マルチシグアカウントを無効化する MultisigAccountModificationTransactionV2
変更をマルチシグトランザクションでラップする MultisigTransactionV1