Sending transactions

Sending a transfer transaction

The goal of this guide is to create a transfer transaction, one of the most commonly used actions in NEM.

Then, you will sign and announce the transaction to the network.

Prerequisites

Background

../_images/guides-transactions-transfer.png

Sending a transfer Transaction

Alice wants to send 10 XEM to Bob, who just created a NEM account with address SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54.

The transfer transaction is used to send mosaics between two accounts. It can hold a message of length 1024.

Monitoring the transaction

Before starting this example, it is advisable to see what happens when sending transactions to the blockchain.

We suggest opening three new terminals, and type the following commands:

The first terminal monitors account transactions validation errors.

$> nem2-cli monitor status --address <your-address-here>

Monitoring unconfirmed shows you in real-time which transactions have reached the network, but are not valid yet.

$> nem2-cli monitor unconfirmed --address <your-address-here>

See when a transaction involving the provided address gets confirmed.

$> nem2-cli monitor confirmed --address <your-address-here>

Let’s get into some code

Alice sends 10 XEM to Bob. She can also include a message, for example Welcome to NEM.

const recipientAddress = 'SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54';

const transferTransaction = TransferTransaction.create(
    Deadline.create(),
    Address.createFromRawAddress(recipientAddress),
    [XEM.createRelative(10)],
    PlainMessage.create('Welcome To NEM'),
    NetworkType.MIJIN_TEST,
);
        final String recipientAddress = "SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54";

        final TransferTransaction transferTransaction = TransferTransaction.create(
            Deadline.create(2, HOURS),
            Address.createFromRawAddress(recipientAddress),
            Collections.singletonList(XEM.createRelative(BigInteger.valueOf(10))),
            PlainMessage.create("Welcome To NEM"),
            NetworkType.MIJIN_TEST
        );
const recipientAddress = 'SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54';

const transferTransaction = TransferTransaction.create(
    Deadline.create(),
    Address.createFromRawAddress(recipientAddress),
    [XEM.createRelative(10)],
    PlainMessage.create('Welcome To NEM'),
    NetworkType.MIJIN_TEST,
);

Although the transaction is created, it has not been announced to the network yet.

To announce it, Alice shall sign the transaction with her account first so that the network can verify its authenticity.

// Replace with private key
const privateKey = process.env.PRIVATE_KEY as string;

const account = Account.createFromPrivateKey(privateKey,NetworkType.MIJIN_TEST);

const signedTransaction = account.sign(transferTransaction);
        // Replace with private key
        final String privateKey = "";

        final Account account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

        final SignedTransaction signedTransaction = account.sign(transferTransaction);
// Replace with private key
const privateKey = process.env.PRIVATE_KEY;

const account = Account.createFromPrivateKey(privateKey,NetworkType.MIJIN_TEST);

const signedTransaction = account.sign(transferTransaction);

Once signed, Alice can announce the transaction to the network.

const transactionHttp = new TransactionHttp('http://localhost:3000');

transactionHttp.announce(signedTransaction).subscribe(x => console.log(x),
    err => console.error(err));
        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        transactionHttp.announce(signedTransaction).toFuture().get();
const transactionHttp = new TransactionHttp('http://localhost:3000');

transactionHttp.announce(signedTransaction).subscribe(x => console.log(x),
    err => console.error(err));
 nem2-cli transaction transfer --recipient SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54 --mosaics nem:xem::10000000 --message "Welcome to NEM"

Open the terminal where you are monitoring account transactions status, it should be empty. If you see an error, check HTTP Error Codes and their meaning.

A new transaction should have appeared in the terminal where you are monitoring unconfirmed. At this point, the transaction has reached the network, but it is not clear if it will get included in a block.

If it is included in a block, the transaction gets processed, and the amount stated in the transaction gets transferred from the sender’s account to the recipient’s account. Additionally, the transaction fee is deducted from the sender’s account.

What’s next?

Afterwards, try to send new transfer transactions by altering your code to send multiple mosaics in the same transaction.

Adding multiple mosaics

../_images/guides-transactions-transfer-multiple.png

Sending multiple mosaics in the same transaction

As you may have noticed, transfer transactions require an array of mosaics as a parameter, allowing to create transfer transactions with multiple mosaics at a time.

If you own more than one mosaic, try to send them together in the same transaction by changing the array:

    [
        new Mosaic( new MosaicId('alice:token'), UInt64.fromUint(10)),
        XEM.createRelative(10),
    ],
                Arrays.asList(
                        new Mosaic(new MosaicId("alice:token"), BigInteger.valueOf(10)),
                        XEM.createRelative(BigInteger.valueOf(10))
                ),
    [
        new Mosaic( new MosaicId('alice:token'), UInt64.fromUint(10)),
        XEM.createRelative(10),
    ],
nem2-cli transaction transfer --recipient SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54 --mosaics alice:token::10,nem:xem::10000000 --message "sending multiple mosaics"

Sending a multisig transaction

The purpose of this guide is to create a multisig transaction, learning how aggregate bonded transaction works.

Then, you will sign and announce the transaction to the network.

Background

../_images/guides-transactions-multisig.png

Sending an aggregate complete transaction

Alice and Bob live together and have separate accounts. They also have a shared account so that if Bob is out shopping, he can buy groceries for both him and Alice.

This shared account is in NEM translated as 1-of-2 multisig, meaning that one cosignatory needs to cosign the transaction to be included in a block.

Remember that a multisig account has cosignatories accounts, and it cannot start transactions itself. Only the cosignatories can initiate transactions.

Prerequisites

Let’s get into some code

Bob has finished filling his basket, and he is ready to pay. The cashier’s screen indicates that the cost of the purchase is 10 XEM.

Bob needs to know which is the public key of the multisig account that he shares with Alice, and his private key to start announcing the transaction.

// Replace with the cosignatory private key
const cosignatoryPrivateKey = process.env.COSIGNATORY_1_PRIVATE_KEY as string;

// Replace with the multisig public key
const multisigAccountPublicKey = '202B3861F34F6141E120742A64BC787D6EBC59C9EFB996F4856AA9CBEE11CD31';

// Replace with recipient address
const recipientAddress = 'SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54';

const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.MIJIN_TEST);

const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.MIJIN_TEST);
        // Replace with a Cosignatory's private key
        final String cosignatoryPrivateKey = "";

        // Replace with a Multisig's public key
        final String multisigAccountPublicKey = "";

        // Replace with recipient address
        final String recipientAddress = "SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54";

        final Account cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.MIJIN_TEST);

        final PublicAccount multisigPublicAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.MIJIN_TEST);
// Replace with the cosignatory private key
const cosignatoryPrivateKey = process.env.COSIGNATORY_1_PRIVATE_KEY;

// Replace with the multisig public key
const multisigAccountPublicKey = '202B3861F34F6141E120742A64BC787D6EBC59C9EFB996F4856AA9CBEE11CD31';

// Replace with recipient address
const recipientAddress = 'SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54';

const multisigAccount = PublicAccount.createFromPublicKey(multisigAccountPublicKey, NetworkType.MIJIN_TEST);

const cosignatoryAccount = Account.createFromPrivateKey(cosignatoryPrivateKey, NetworkType.MIJIN_TEST);

As he wants to pay the groceries with the multisig account, he defines a transfer transaction.

  • Recipient: Grocery’s address
  • Message: Grocery payment
  • Mosaics: [10 XEM]
const transferTransaction = TransferTransaction.create(
    Deadline.create(),
    Address.createFromRawAddress(recipientAddress),
    [XEM.createRelative(10)],
    PlainMessage.create('sending 10 nem:xem'),
    NetworkType.MIJIN_TEST
);
        final TransferTransaction transferTransaction = TransferTransaction.create(
                Deadline.create(2, HOURS),
                Address.createFromRawAddress(recipientAddress),
                Collections.singletonList(XEM.createRelative(BigInteger.valueOf(10))),
                PlainMessage.create("sending 10 nem:xem"),
                NetworkType.MIJIN_TEST
        );
const transferTransaction = TransferTransaction.create(
    Deadline.create(),
    Address.createFromRawAddress(recipientAddress),
    [XEM.createRelative(10)],
    PlainMessage.create('sending 10 nem:xem'),
    NetworkType.MIJIN_TEST
);

Wrap the transfer transaction under an aggregate transaction, attaching multisig public key as the signer.

An aggregate transaction is complete if before announcing it to the network, all required cosigners have signed it. If valid, it will be included in a block.

Remember that we are using a 1-of-2 multisig account? As Bob has one private key to sign the transaction, consider an aggregate complete transaction.

const aggregateTransaction = AggregateTransaction.createComplete(
    Deadline.create(),
    [
        transferTransaction.toAggregate(multisigAccount),
    ],
    NetworkType.MIJIN_TEST,
    []
);
        final AggregateTransaction aggregateTransaction = AggregateTransaction.createComplete(
                Deadline.create(2, HOURS),
                Collections.singletonList(
                        transferTransaction.toAggregate(multisigPublicAccount)
                ),
                NetworkType.MIJIN_TEST
        );
const aggregateTransaction = AggregateTransaction.createComplete(
    Deadline.create(),
    [
        transferTransaction.toAggregate(multisigAccount),
    ],
    NetworkType.MIJIN_TEST,
    []
);

Then, he signs and announces the transaction.

const signedTransaction = cosignatoryAccount.sign(aggregateTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

transactionHttp.announce(signedTransaction).subscribe(x => console.log(x),
    err => console.error(err));
        final SignedTransaction aggregateSignedTransaction = cosignatoryAccount.sign(aggregateTransaction);


        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        transactionHttp.announce(aggregateSignedTransaction).toFuture().get();
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

transactionHttp.announce(signedTransaction).subscribe(x => console.log(x),
    err => console.error(err));

What’s next?

What would have happened if the account were a 2-of-2 multisig instead of a 1-of-2?

As all required cosigners didn’t sign the transaction, it should be announced as aggregate bonded.

../_images/guides-transactions-multisig-2.png

Sending an aggregate bonded transaction

const aggregateTransaction = AggregateTransaction.createBonded(
    Deadline.create(),
    [
        transferTransaction.toAggregate(multisigAccount),
    ],
    NetworkType.MIJIN_TEST
);
        final AggregateTransaction aggregateTransaction = AggregateTransaction.createBonded(
                Deadline.create(2, HOURS),
                Arrays.asList(
                        transferTransaction.toAggregate(multisigPublicAccount)
                ),
                NetworkType.MIJIN_TEST
        );
const aggregateTransaction = AggregateTransaction.createBonded(
    Deadline.create(),
    [
        transferTransaction.toAggregate(multisigAccount),
    ],
    NetworkType.MIJIN_TEST
);

Open a new terminal for monitoring the aggregate bonded transaction.

$> nem2-cli monitor aggregatebonded --address <your-address-here>

When an aggregate transaction is bonded, Bob needs to lock at least 10 XEM.

Once all cosigners sign the transaction, the amount of XEM becomes available again on Bob’s account.

After locks fund transaction has been confirmed, announce the aggregate bonded transaction.

//Signing the aggregate transaction
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction);

//Creating the lock funds transaction and announce it

const lockFundsTransaction = LockFundsTransaction.create(
    Deadline.create(),
    XEM.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const lockFundsTransactionSigned = cosignatoryAccount.sign(lockFundsTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

// announce signed transaction
const listener = new Listener('http://localhost:3000');

listener.open().then(() => {

    transactionHttp.announce(lockFundsTransactionSigned).subscribe(x => console.log(x),
        err => console.error(err));

    listener.confirmed(cosignatoryAccount.address)
        .filter((transaction) => transaction.transactionInfo !== undefined
            && transaction.transactionInfo.hash === lockFundsTransactionSigned.hash)
        .flatMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
        .subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
            err => console.error(err));
});
        final SignedTransaction aggregateSignedTransaction = cosignatoryAccount.sign(aggregateTransaction);

        // Creating the lock funds transaction and announce it

        final LockFundsTransaction lockFundsTransaction = LockFundsTransaction.create(
                Deadline.create(2, HOURS),
                XEM.createRelative(BigInteger.valueOf(10)),
                BigInteger.valueOf(480),
                aggregateSignedTransaction,
                NetworkType.MIJIN_TEST
        );

        final SignedTransaction lockFundsTransactionSigned = cosignatoryAccount.sign(lockFundsTransaction);

        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        transactionHttp.announce(lockFundsTransactionSigned).toFuture().get();

        System.out.println(lockFundsTransactionSigned.getHash());

        final Listener listener = new Listener("http://localhost:3000");

        listener.open().get();

        final Transaction transaction = listener.confirmed(cosignatoryAccount.getAddress()).take(1).toFuture().get();

        System.out.println(transaction);

        transactionHttp.announceAggregateBonded(aggregateSignedTransaction).toFuture().get();
//Signing the aggregate transaction
const signedTransaction = cosignatoryAccount.sign(aggregateTransaction);

//Creating the lock funds transaction and announce it

const lockFundsTransaction = LockFundsTransaction.create(
    Deadline.create(),
    XEM.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const lockFundsTransactionSigned = cosignatoryAccount.sign(lockFundsTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

// announce signed transaction
const listener = new Listener('http://localhost:3000');

listener.open().then(() => {

    transactionHttp.announce(lockFundsTransactionSigned).subscribe(x => console.log(x),
        err => console.error(err));

    listener.confirmed(cosignatoryAccount.address)
        .filter((transaction) => transaction.transactionInfo !== undefined
            && transaction.transactionInfo.hash === lockFundsTransactionSigned.hash)
        .flatMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
        .subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
            err => console.error(err));
});

Alice should cosign the transaction to be confirmed!

Sending payouts with aggregate complete transaction

This guide explains how to send transactions to different accounts atomically, using an aggregate complete transaction.

Background

It is Christmas, and uncle Dan wants to send money to their nephews Alice and Bob.

../_images/guides-transactions-sending-payouts1.png

Sending transactions to different recipients atomically

He chooses to send an aggregate complete transaction, so both of them will receive the funds at the same time.

Prerequisites

Let’s get into some code?

First, Dan creates two transfer transaction with two different recipients, and wrap them in an aggregate transaction.

// Replace with private key
const privateKey = process.env.PRIVATE_KEY as string;

// Replace with addresses
const brotherAddress = 'SDG4WG-FS7EQJ-KFQKXM-4IUCQG-PXUW5H-DJVIJB-OXJG';
const sisterAddress = 'SCGPXB-2A7T4I-W5MQCX-FQY4UQ-W5JNU5-F55HGK-HBUN';

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

const brotherAccount = Address.createFromRawAddress(brotherAddress);
const sisterAccount = Address.createFromRawAddress(sisterAddress);

const amount = XEM.createRelative(10); // 10 xem represent 10 000 000 micro xem

const brotherTransferTransaction = TransferTransaction.create(Deadline.create(), brotherAccount, [amount], PlainMessage.create('payout'), NetworkType.MIJIN_TEST);
const sisterTransferTransaction = TransferTransaction.create(Deadline.create(), sisterAccount, [amount], PlainMessage.create('payout'), NetworkType.MIJIN_TEST);

const aggregateTransaction = AggregateTransaction.createComplete(
    Deadline.create(),
    [
        brotherTransferTransaction.toAggregate(account.publicAccount),
        sisterTransferTransaction.toAggregate(account.publicAccount)],
    NetworkType.MIJIN_TEST,
    []
);
        // Replace with private key
        final String privateKey = "";

        final Address brotherAddress = Address.createFromRawAddress("SDG4WG-FS7EQJ-KFQKXM-4IUCQG-PXUW5H-DJVIJB-OXJG");
        final Address sisterAddress = Address.createFromRawAddress("SCGPXB-2A7T4I-W5MQCX-FQY4UQ-W5JNU5-F55HGK-HBUN");

        final Account account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

        final XEM xem = XEM.createRelative(BigInteger.valueOf(10)); // 10 xem represent 10 000 000 micro xem

        final TransferTransaction brotherTransferTransaction = TransferTransaction.create(
                Deadline.create(2, HOURS),
                brotherAddress,
                Collections.singletonList(xem),
                PlainMessage.create("payout"),
                NetworkType.MIJIN_TEST
        );

        final TransferTransaction sisterTransferTransaction = TransferTransaction.create(
                Deadline.create(2, HOURS),
                sisterAddress,
                Collections.singletonList(xem),
                PlainMessage.create("payout"),
                NetworkType.MIJIN_TEST
        );

        final AggregateTransaction aggregateTransaction = AggregateTransaction.createComplete(
                Deadline.create(2, HOURS),
                Arrays.asList(
                        brotherTransferTransaction.toAggregate(account.getPublicAccount()),
                        sisterTransferTransaction.toAggregate(account.getPublicAccount())
                ),
                NetworkType.MIJIN_TEST
        );
// Replace with private key
const privateKey = process.env.PRIVATE_KEY;

// Replace with addresses
const brotherAddress = 'SDG4WG-FS7EQJ-KFQKXM-4IUCQG-PXUW5H-DJVIJB-OXJG';
const sisterAddress = 'SCGPXB-2A7T4I-W5MQCX-FQY4UQ-W5JNU5-F55HGK-HBUN';

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

const brotherAccount = Address.createFromRawAddress(brotherAddress);
const sisterAccount = Address.createFromRawAddress(sisterAddress);

const amount = XEM.createRelative(10); // 10 xem represent 10 000 000 micro xem

const brotherTransferTransaction = TransferTransaction.create(Deadline.create(), brotherAccount, [amount], PlainMessage.create('payout'), NetworkType.MIJIN_TEST);
const sisterTransferTransaction = TransferTransaction.create(Deadline.create(), sisterAccount, [amount], PlainMessage.create('payout'), NetworkType.MIJIN_TEST);

const aggregateTransaction = AggregateTransaction.createComplete(
    Deadline.create(),
    [
        brotherTransferTransaction.toAggregate(account.publicAccount),
        sisterTransferTransaction.toAggregate(account.publicAccount)],
    NetworkType.MIJIN_TEST,
    []
);

Do you know the difference between aggregate complete and aggregate bonded? In this case, one private key can sign all the transactions in the aggregate, so it is aggregate complete.

That means that there is no need to lock funds to send the transaction. If valid, it will be accepted by the network.

Just sign it and announce it!

const transactionHttp = new TransactionHttp('http://localhost:3000');

const signedTransaction = account.sign(aggregateTransaction);

transactionHttp.announce(signedTransaction).subscribe(x => console.log(x),
        err => console.error(err));
        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        final SignedTransaction signedTransaction = account.sign(aggregateTransaction);

        transactionHttp.announce(signedTransaction).toFuture().get();
const transactionHttp = new TransactionHttp('http://localhost:3000');

const signedTransaction = account.sign(aggregateTransaction);

transactionHttp.announce(signedTransaction).subscribe(x => console.log(x),
        err => console.error(err));

What’s next?

Try to send an aggregate bonded transaction by following creating an escrow with aggregate bonded transaction guide.

Creating an escrow with aggregate bonded transaction

This guide will run you through NEM general concepts, and precisely aggregate bonded transaction, by creating an escrow.

Background

An escrow is a contractual arrangement in which a third party receives and disburses money or documents for the primary transacting parties, with the disbursement dependent on conditions agreed to by the transacting parties, or an account established by a broker for holding funds on behalf of the broker’s principal or some other person until the consummation or termination of a transaction; or, a trust account held in the borrower’s name to pay obligations such as property taxes and insurance premiums.

See full description at Wikipedia.

In this example, imagine the two parties agree in a virtual service, implying that the escrow can be immediate.

How does it work?

  1. Buyer and seller agree to terms
  2. Buyer submits payment to escrow
  3. Seller delivers goods or service to Buyer
  4. Buyer approves goods or service
  5. Escrow releases payment to the seller

How is it applied to NEM?

Normalizing the language into NEM related concepts:

contractual arrangement
Aggregate Transaction
third party receives and disburses money
No third party
primary transacting parties
Accounts
conditions agreed to by the transacting parties
Sign transaction
account established by a broker for holding funds
No account, just an atomic swap
until the consummation or termination of a transaction
The transaction included in a block

Prerequisites

Let’s get into some code?

../_images/guides-transactions-escrow1.png

Multi-Asset Escrowed Transactions

Setting up the required accounts and mosaics

In this example, Alice and a ticket distributor want to swap the following assets.

Owner Mosaic Name Amount
Alice nem:xem 100
Ticket distributor museum:ticket 1

Before continuing, practise by setting up set up the namespaces and mosaics required.

Mosaics swap

Alice will send a transaction to the ticket distributor exchanging 100 nem:xem with 1 museum:ticket.

Create two transfer transaction:

  1. From Alice to the ticket distributor sending 100 nem:xem
  2. From the ticket distributor to Alice sending 1 museum:ticket.

Add them as innerTransactions under an aggregate transaction.

An aggregate Transaction is complete if before announcing it to the network, all required cosigners have signed it. If valid, it will be included in a block.

In case that requires signatures from other participants but announced to the network, then the transaction is considered aggregate bonded.

// Replace with private key
const alicePrivateKey = process.env.PRIVATE_KEY as string;

// Replace with public key
const ticketDistributorPublicKey = 'F82527075248B043994F1CAFD965F3848324C9ABFEC506BC05FBCF5DD7307C9D';

const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MIJIN_TEST);
const ticketDistributorPublicAccount = PublicAccount.createFromPublicKey( ticketDistributorPublicKey, NetworkType.MIJIN_TEST);

const aliceToTicketDistributorTx = TransferTransaction.create(
    Deadline.create(),
    ticketDistributorPublicAccount.address,
    [XEM.createRelative(100)],
    PlainMessage.create('send 100 nem:xem to distributor'),
    NetworkType.MIJIN_TEST,
);

const ticketDistributorToAliceTx = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [new Mosaic( new MosaicId('museum:ticket'), UInt64.fromUint(1))],
    PlainMessage.create('send 1 museum:ticket to alice'),
    NetworkType.MIJIN_TEST,
);

const aggregateTransaction = AggregateTransaction.createBonded(Deadline.create(),
    [
        aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
        ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount),
    ],
    NetworkType.MIJIN_TEST);


const signedTransaction = aliceAccount.sign(aggregateTransaction);
        // Replace with private key
        final String alicePrivateKey = "";

        // Replace with public key
        final String ticketDistributorPublicKey = "";

        final Account aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MIJIN_TEST);
        final PublicAccount ticketDistributorPublicAccount = PublicAccount.createFromPublicKey(ticketDistributorPublicKey, NetworkType.MIJIN_TEST);

        final TransferTransaction aliceToTicketDistributorTx = TransferTransaction.create(
                Deadline.create(2, HOURS),
                ticketDistributorPublicAccount.getAddress(),
            Collections.singletonList(XEM.createRelative(BigInteger.valueOf(100))),
                PlainMessage.create("send 100 nem:xem to distributor"),
                NetworkType.MIJIN_TEST
        );

        final TransferTransaction ticketDistributorToAliceTx = TransferTransaction.create(
                Deadline.create(2, HOURS),
                aliceAccount.getAddress(),
                Collections.singletonList(new Mosaic(new MosaicId("museum:ticket"), BigInteger.valueOf(1))),
                PlainMessage.create("send 1 museum:ticket to alice"),
                NetworkType.MIJIN_TEST
        );

        final AggregateTransaction aggregateTransaction = AggregateTransaction.createBonded(
                Deadline.create(2, HOURS),
                Arrays.asList(
                        aliceToTicketDistributorTx.toAggregate(aliceAccount.getPublicAccount()),
                        ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount)
                ),
                NetworkType.MIJIN_TEST
        );

        final SignedTransaction aggregateSignedTransaction = aliceAccount.sign(aggregateTransaction);
// Replace with private key
const alicePrivateKey = process.env.PRIVATE_KEY;

// Replace with public key
const ticketDistributorPublicKey = 'F82527075248B043994F1CAFD965F3848324C9ABFEC506BC05FBCF5DD7307C9D';

const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MIJIN_TEST);
const ticketDistributorPublicAccount = PublicAccount.createFromPublicKey( ticketDistributorPublicKey, NetworkType.MIJIN_TEST);

const aliceToTicketDistributorTx = TransferTransaction.create(
    Deadline.create(),
    ticketDistributorPublicAccount.address,
    [XEM.createRelative(100)],
    PlainMessage.create('send 100 nem:xem to distributor'),
    NetworkType.MIJIN_TEST,
);

const ticketDistributorToAliceTx = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [new Mosaic( new MosaicId('museum:ticket'), UInt64.fromUint(1))],
    PlainMessage.create('send 1 museum:ticket to alice'),
    NetworkType.MIJIN_TEST,
);

const aggregateTransaction = AggregateTransaction.createBonded(Deadline.create(),
    [
        aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
        ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount),
    ],
    NetworkType.MIJIN_TEST);


const signedTransaction = aliceAccount.sign(aggregateTransaction);

When an aggregate transaction is bonded, Alice will need to lock at least 10 XEM.

Once the ticket distributor signs the aggregate transaction, the amount of locked XEM becomes available again on Alice’s account, and the exchange will get through.

// Creating the lock funds transaction

const lockFundsTransaction = LockFundsTransaction.create(
    Deadline.create(),
    XEM.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const lockFundsTransactionSigned = aliceAccount.sign(lockFundsTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

// announce signed transaction
const listener = new Listener('http://localhost:3000');

listener.open().then(() => {

    transactionHttp.announce(lockFundsTransactionSigned).subscribe(x => console.log(x),
        err => console.error(err));

    listener.confirmed(aliceAccount.address)
        .filter((transaction) => transaction.transactionInfo !== undefined
            && transaction.transactionInfo.hash === lockFundsTransactionSigned.hash)
        .flatMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
        .subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
            err => console.error(err));
});
        // Creating the lock funds transaction and announce it

        final LockFundsTransaction lockFundsTransaction = LockFundsTransaction.create(
                Deadline.create(2, HOURS),
                XEM.createRelative(BigInteger.valueOf(10)),
                BigInteger.valueOf(480),
                aggregateSignedTransaction,
                NetworkType.MIJIN_TEST
        );

        final SignedTransaction lockFundsTransactionSigned = aliceAccount.sign(lockFundsTransaction);

        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        transactionHttp.announce(lockFundsTransactionSigned).toFuture().get();

        System.out.println(lockFundsTransactionSigned.getHash());

        final Listener listener = new Listener("http://localhost:3000");

        listener.open().get();

        final Transaction transaction = listener.confirmed(aliceAccount.getAddress()).take(1).toFuture().get();

        transactionHttp.announceAggregateBonded(aggregateSignedTransaction).toFuture().get();
// Creating the lock funds transaction

const lockFundsTransaction = LockFundsTransaction.create(
    Deadline.create(),
    XEM.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const lockFundsTransactionSigned = aliceAccount.sign(lockFundsTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

// announce signed transaction
const listener = new Listener('http://localhost:3000');

listener.open().then(() => {

    transactionHttp.announce(lockFundsTransactionSigned).subscribe(x => console.log(x),
        err => console.error(err));

    listener.confirmed(aliceAccount.address)
        .filter((transaction) => transaction.transactionInfo !== undefined
            && transaction.transactionInfo.hash === lockFundsTransactionSigned.hash)
        .flatMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
        .subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
            err => console.error(err));
});

Is it possible without aggregate transactions?

It is not secure, since something of the next list could happen:

  • The buyer doesn’t pay.
  • The seller doesn’t send the virtual goods.

What’s next?

The distributor didn’t sign the aggregate bonded transaction yet, so exchange has not been completed. Consider reading signing announced aggregate bonded transactions guide.

Afterwards, swap mosaics between multiple participants.

../_images/guides-transactions-escrow-2.png

Multi-Asset Escrowed Transactions

Asking for mosaics with aggregate bonded transaction

This guide shows you how to ask an account to send you some mosaics using an aggregate bonded transaction.

Prerequisites

Let’s get into some code

../_images/guides-transactions-asking-for-mosaics.png

Asking for mosaics with an aggregate bonded transaction

Alice wants to ask Bob for 20 XEM.

Alice creates an aggregate bonded transaction with two inner transactions:

Inner transfer transaction 1:

  • message: “message reason” (custom, but not empty)
  • receiver: Bob address
  • signer: Alice
const transferTransaction1 = TransferTransaction.create(
    Deadline.create(),
    bobAccount.address,
    [],
    PlainMessage.create('send me 20 XEM'),
    NetworkType.MIJIN_TEST,
);
        final TransferTransaction transferTransaction1 = TransferTransaction.create(
            Deadline.create(2, HOURS),
            bobPublicAccount.getAddress(),
            Collections.emptyList(),
            PlainMessage.create("send me 20 XEM"),
            NetworkType.MIJIN_TEST
        );
const transferTransaction1 = TransferTransaction.create(
    Deadline.create(),
    bobAccount.address,
    [],
    PlainMessage.create('send me 20 XEM'),
    NetworkType.MIJIN_TEST,
);

Inner transfer transaction 2:

  • message: empty
  • receiver: Alice address
  • mosaics: 20 XEM
  • signer: Bob
const transferTransaction2 = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [XEM.createRelative(20)],
    EmptyMessage,
    NetworkType.MIJIN_TEST,
);
        final TransferTransaction transferTransaction2 = TransferTransaction.create(
                Deadline.create(2, HOURS),
                aliceAccount.getAddress(),
                Collections.singletonList(XEM.createRelative(BigInteger.valueOf(20))),
                PlainMessage.Empty,
                NetworkType.MIJIN_TEST
        );
const transferTransaction2 = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [XEM.createRelative(20)],
    EmptyMessage,
    NetworkType.MIJIN_TEST,
);

Aggregate transaction:

const aggregateTransaction = AggregateTransaction.createBonded(
    Deadline.create(),
    [
        transferTransaction1.toAggregate(aliceAccount.publicAccount),
        transferTransaction2.toAggregate(bobAccount)
    ],
    NetworkType.MIJIN_TEST
);
        final AggregateTransaction pullTransaction = AggregateTransaction.createBonded(
            Deadline.create(2, HOURS),
            Arrays.asList(
                transferTransaction1.toAggregate(aliceAccount.getPublicAccount()),
                transferTransaction2.toAggregate(bobPublicAccount)
            ),
            NetworkType.MIJIN_TEST
        );
const aggregateTransaction = AggregateTransaction.createBonded(
    Deadline.create(),
    [
        transferTransaction1.toAggregate(aliceAccount.publicAccount),
        transferTransaction2.toAggregate(bobAccount)
    ],
    NetworkType.MIJIN_TEST
);

Alice signs the aggregate bonded transaction and announces it to the network, locking first 10 XEM.

const signedTransaction = aliceAccount.sign(aggregateTransaction);

//Creating the lock funds transaction and announce it
const lockFundsTransaction = LockFundsTransaction.create(
    Deadline.create(),
    XEM.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const lockFundsTransactionSigned = aliceAccount.sign(lockFundsTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

const listener = new Listener('http://localhost:3000');

listener.open().then(() => {

    transactionHttp.announce(lockFundsTransactionSigned).subscribe(
        x => console.log(x),
        err => console.error(err));

    listener.confirmed(aliceAccount.address)
        .filter((transaction) => transaction.transactionInfo !== undefined
            && transaction.transactionInfo.hash === lockFundsTransactionSigned.hash)
        .flatMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
        .subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
            err => console.error(err));
});
        final SignedTransaction pullTransactionSigned = aliceAccount.sign(pullTransaction);

        // Creating the lock funds transaction and announce it
        final LockFundsTransaction lockFundsTransaction = LockFundsTransaction.create(
            Deadline.create(2, HOURS),
            XEM.createRelative(BigInteger.valueOf(10)),
            BigInteger.valueOf(480),
            pullTransactionSigned,
            NetworkType.MIJIN_TEST
        );

        final SignedTransaction lockFundsTransactionSigned = aliceAccount.sign(lockFundsTransaction);

        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        transactionHttp.announce(lockFundsTransactionSigned).toFuture().get();

        System.out.println(lockFundsTransactionSigned.getHash());

        final Listener listener = new Listener("http://localhost:3000");

        listener.open().get();

        final Transaction transaction = listener.confirmed(aliceAccount.getAddress()).take(1).toFuture().get();

        transactionHttp.announceAggregateBonded(pullTransactionSigned).toFuture().get();
const signedTransaction = aliceAccount.sign(aggregateTransaction);

//Creating the lock funds transaction and announce it
const lockFundsTransaction = LockFundsTransaction.create(
    Deadline.create(),
    XEM.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const lockFundsTransactionSigned = aliceAccount.sign(lockFundsTransaction);

const transactionHttp = new TransactionHttp('http://localhost:3000');

const listener = new Listener('http://localhost:3000');

listener.open().then(() => {

    transactionHttp.announce(lockFundsTransactionSigned).subscribe(
        x => console.log(x),
        err => console.error(err));

    listener.confirmed(aliceAccount.address)
        .filter((transaction) => transaction.transactionInfo !== undefined
            && transaction.transactionInfo.hash === lockFundsTransactionSigned.hash)
        .flatMap(ignored => transactionHttp.announceAggregateBonded(signedTransaction))
        .subscribe(announcedAggregateBonded => console.log(announcedAggregateBonded),
            err => console.error(err));
});

If all goes well, Bob receives a notification via WebSocket (or fetched via API Http request).

What’s next?

Bob didn’t cosign the transaction yet. Consider reading signing announced aggregate bonded transactions guide.

After receiving the transaction, Bob signs the transaction hash and announces the cosignature signed transaction.

As the aggregate bonded transaction has all the cosignatures required, it will be included in a block.

Signing announced aggregate bonded transactions

You probably have announced an aggregate bonded transaction, but all cosigners have not signed it yet.

This guide will help you to cosign aggregate bonded transactions that require being signed by your account.

Prerequisites

Let’s get into some code

Create a function to cosign any aggregate bonded transaction.

const cosignAggregateBondedTransaction = (transaction:AggregateTransaction, account: Account) : CosignatureSignedTransaction => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};
const cosignAggregateBondedTransaction = (transaction, account)  => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};

Fetch all aggregate bonded transactions pending to be signed by your account.

Note

To fetch aggregate bonded transactions that should be signed by multisig cosignatories, refer to the multisig public key instead. See how to get multisig accounts where an account is cosignatory.

For each transaction, check if you have not already signed it. Cosign each pending transaction using the previously created function.

Did you realise that we are using RxJS operators intensively? Announce CosignatureSignedTransaction to the network using the TransactionHttp repository.

// Replace with private key
const privateKey = process.env.PRIVATE_KEY as string;

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);
const accountHttp = new AccountHttp('http://localhost:3000');
const transactionHttp = new TransactionHttp('http://localhost:3000');


accountHttp.aggregateBondedTransactions(account.publicAccount)
    .flatMap((_) => _)
    .filter((_) => !_.signedByAccount(account.publicAccount))
    .map(transaction => cosignAggregateBondedTransaction(transaction, account))
    .flatMap(cosignatureSignedTransaction => transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction))
    .subscribe(announcedTransaction => console.log(announcedTransaction),
        err => console.error(err));
        // Replace with a private key
        final String privateKey = "";

        final Account account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

        final AccountHttp accountHttp = new AccountHttp("http://localhost:3000");

        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        accountHttp.aggregateBondedTransactions(account.getPublicAccount())
                .flatMapIterable(tx -> tx) // Transform transaction array to single transactions to process them
                .filter(tx -> !tx.signedByAccount(account.getPublicAccount()))
                .map(tx -> {
                  final CosignatureTransaction cosignatureTransaction = CosignatureTransaction.create(tx);

                  final CosignatureSignedTransaction cosignatureSignedTransaction = account.signCosignatureTransaction(cosignatureTransaction);

                  return transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction).toFuture().get();
                })
                .toFuture()
                .get();
// Replace with private key
const privateKey = process.env.PRIVATE_KEY;

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);
const accountHttp = new AccountHttp('http://localhost:3000');
const transactionHttp = new TransactionHttp('http://localhost:3000');


accountHttp.aggregateBondedTransactions(account.publicAccount)
    .flatMap((_) => _)
    .filter((_) => !_.signedByAccount(account.publicAccount))
    .map(transaction => cosignAggregateBondedTransaction(transaction, account))
    .flatMap(cosignatureSignedTransaction => transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction))
    .subscribe(announcedTransaction => console.log(announcedTransaction),
        err => console.error(err));

Signing announced aggregate bonded transactions automatically

Following this guide, you will create an application that is notified every time your account receives a transaction pending to be cosigned.

Automatically, the app will cosign the transaction and announce it to the network.

Prerequisites

Let’s get into some code

Create a function to cosign any aggregate bonded transaction.

const cosignAggregateBondedTransaction = (transaction:AggregateTransaction, account: Account) : CosignatureSignedTransaction => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};
const cosignAggregateBondedTransaction = (transaction, account)  => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};

Create a new listener to get notified every time a new aggregate bonded transaction requires the signature of your account.

Open the connection. You only need to open the connection once and then connect to all desired channels.

Start listening for new transactions, subscribing to the aggregateBondedAdded channel using your account’s address.

Note

To sign automatically aggregate bonded transactions that should be signed by multisig cosignatories, refer to the multisig address instead. See how to get multisig accounts where an account is cosignatory.

For each received transaction, check if you have not already signed it. Cosign each pending aggregate bonded transaction using the previously created function.

Did you realise that we are using RxJS operators intensively? Announce CosignatureSignedTransaction to the network using the TransactionHttp repository.

// Replace with private key
const privateKey = process.env.PRIVATE_KEY as string;

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

const listener = new Listener('http://localhost:3000');

const transactionHttp = new TransactionHttp('http://localhost:3000');

listener.open().then(() => {

    listener.aggregateBondedAdded(account.address)
        .filter((_) => !_.signedByAccount(account.publicAccount))
        .map(transaction => cosignAggregateBondedTransaction(transaction, account))
        .flatMap(cosignatureSignedTransaction => transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction))
        .subscribe(announcedTransaction => console.log(announcedTransaction),
            err => console.error(err));
});
        // Replace with a private key
        final String privateKey = "";

        final Account account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

        final AccountHttp accountHttp = new AccountHttp("http://localhost:3000");

        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        final Listener listener = new Listener("http://localhost:3000");

        listener.open().get();

        final AggregateTransaction transaction = listener.aggregateBondedAdded(account.getAddress()).take(1).toFuture().get();

        if (!transaction.signedByAccount(account.getPublicAccount())) {
            // Filter aggregates that need my signature
            final CosignatureTransaction cosignatureTransaction = CosignatureTransaction.create(transaction);

            final CosignatureSignedTransaction cosignatureSignedTransaction = account.signCosignatureTransaction(cosignatureTransaction);

            transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction).toFuture().get();
        }
// Replace with private key
const privateKey = process.env.PRIVATE_KEY;

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

const listener = new Listener('http://localhost:3000');

const transactionHttp = new TransactionHttp('http://localhost:3000');

listener.open().then(() => {

    listener.aggregateBondedAdded(account.address)
        .filter((_) => !_.signedByAccount(account.publicAccount))
        .map(transaction => cosignAggregateBondedTransaction(transaction, account))
        .flatMap(cosignatureSignedTransaction => transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction))
        .subscribe(announcedTransaction => console.log(announcedTransaction),
            err => console.error(err));
});

What’s next?

In this guide, you have seen how to create an automatic signature for an account aggregate bonded transactions. Now that you know some general concepts, you could extend it by filtering transactions matching some constraints.

  • Aggregate transactions with two inner transactions.
  • Two inner transactions must be transfer transactions.
  • The transaction sending funds must have yourself as the signer.
  • The transaction sending funds should have only one mosaic being this less than 100 XEM.

Try it yourself! Here you have the implementation:

const validTransaction = (transaction: Transaction, publicAccount: PublicAccount): boolean => {
    return transaction instanceof TransferTransaction &&
        transaction.signer!.equals(publicAccount) &&
        transaction.mosaics.length == 1 &&
        transaction.mosaics[0].id.equals(XEM.MOSAIC_ID) &&
        transaction.mosaics[0].amount.compact() < XEM.createRelative(100).amount.compact();
};

const cosignAggregateBondedTransaction = (transaction: AggregateTransaction, account: Account): CosignatureSignedTransaction => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};

// Replace with private key
const privateKey = process.env.PRIVATE_KEY as string;

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

const listener = new Listener('http://localhost:3000');

const transactionHttp = new TransactionHttp('http://localhost:3000');

listener.open().then(() => {

    listener.aggregateBondedAdded(account.address)
        .filter((_) => _.innerTransactions.length == 2)
        .filter((_) => !_.signedByAccount(account.publicAccount))
        .filter((_) => validTransaction(_.innerTransactions[0], account.publicAccount) || validTransaction(_.innerTransactions[1], account.publicAccount))
        .map(transaction => cosignAggregateBondedTransaction(transaction, account))
        .flatMap(cosignatureSignedTransaction => transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction))
        .subscribe(announcedTransaction => console.log(announcedTransaction),
            err => console.error(err));
});
        final String privateKey = "";

        final Account account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

        final AccountHttp accountHttp = new AccountHttp("http://localhost:3000");

        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");

        final Listener listener = new Listener("http://localhost:3000");

        listener.open().get();

        final AggregateTransaction transaction = listener.aggregateBondedAdded(account.getAddress())
                .filter(tx -> tx.getInnerTransactions().size() == 2)
                .filter(tx -> !tx.signedByAccount(account.getPublicAccount()))
                .filter(tx -> validTransaction(tx.getInnerTransactions().get(0), account.getPublicAccount()) || validTransaction(tx.getInnerTransactions().get(1), account.getPublicAccount()))
                .take(1)
                .toFuture()
                .get();

        final CosignatureTransaction cosignatureTransaction = CosignatureTransaction.create(transaction);
        final CosignatureSignedTransaction cosignatureSignedTransaction = account.signCosignatureTransaction(cosignatureTransaction);

        transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction).toFuture().get();
    }

    private boolean validTransaction(Transaction transaction, PublicAccount publicAccount) {
        return transaction.getType() == TransactionType.TRANSFER &&
                transaction.getSigner().get().equals(publicAccount) &&
                ((TransferTransaction)transaction).getMosaics().size() == 1 &&
                ((TransferTransaction)transaction).getMosaics().get(0).getId().equals(XEM.MOSAICID) &&
                ((TransferTransaction) transaction).getMosaics().get(0).getAmount().compareTo(BigInteger.valueOf(100)) > 0;
    }
const validTransaction = (transaction, publicAccount) => {
    return transaction instanceof TransferTransaction &&
        transaction.signer.equals(publicAccount) &&
        transaction.mosaics.length == 1 &&
        transaction.mosaics[0].id.equals(XEM.MOSAIC_ID) &&
        transaction.mosaics[0].amount.compact() < XEM.createRelative(100).amount.compact();
};

const cosignAggregateBondedTransaction = (transaction, account) => {
    const cosignatureTransaction = CosignatureTransaction.create(transaction);
    return account.signCosignatureTransaction(cosignatureTransaction);
};

// Replace with private key
const privateKey = process.env.PRIVATE_KEY;

const account = Account.createFromPrivateKey(privateKey, NetworkType.MIJIN_TEST);

const listener = new Listener('http://localhost:3000');

const transactionHttp = new TransactionHttp('http://localhost:3000');

listener.open().then(() => {

    listener.aggregateBondedAdded(account.address)
        .filter((_) => _.innerTransactions.length == 2)
        .filter((_) => !_.signedByAccount(account.publicAccount))
        .filter((_) => validTransaction(_.innerTransactions[0], account.publicAccount) || validTransaction(_.innerTransactions[1], account.publicAccount))
        .map(transaction => cosignAggregateBondedTransaction(transaction, account))
        .flatMap(cosignatureSignedTransaction => transactionHttp.announceAggregateBondedCosignature(cosignatureSignedTransaction))
        .subscribe(announcedTransaction => console.log(announcedTransaction),
            err => console.error(err));
});

Using secret lock for atomic cross-chain swap transactions

The goal of this guide is to show you how to exchange tokens between different blockchains atomically using secret lock and secret proof transactions.

Background

Alice and Bob want to exchange 10 alice:token for 10 bob:token. The problem is that they are not in the same blockchain: Alice uses NEM public network, whereas Bob is using MIJIN private network.

One non-atomic solution could be:

  1. Alice sends to Bob 10 alice:token in PUBLIC chain
  2. Bob receives the transaction
  3. Bob sends to Alice 10 bob:token in PRIVATE chain
  4. Alice receives the transaction

But they don’t trust each other that much. Ideally, they want to exchange their tokens between different blockchains atomically.

Following this guide, you will swap transactions between different blockchain platforms atomically using secret lock transaction.

../_images/guides-transactions-atomic-cross-chain-swap1.png

Atomic cross-chain trading between public and private network

Note

Mijin and NEM share SDK. The example will work with other blockchain platforms if implements Secret Lock / Secret Proof transactions mechanism.

Prerequisites

Let’s get into some code?

Alice picks a random number, which we call proof. Applies SHA512 hash algorithm to it, obtaining the secret.

// Alice is a PUBLIC net user
const alicePrivateKey = process.env.ALICE_PRIVATE_KEY as string;
const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MAIN_NET);

// Alice picks a random number and hashes it.
const random = crypto.randomBytes(10);
const hash = sha3_512.create();
const secret = hash.update(random).hex().toUpperCase();
const proof = random.toString('hex');
        // Alice is a PUBLIC net user
        final String alicePrivateKey = "";
        final Account aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MAIN_NET);

        // Alice picks a random number and hashes it.
        final byte[] secretBytes = new byte[20];
        new Random().nextBytes(secretBytes);
        final byte[] result = Hashes.sha3_512(secretBytes);
        final String secret = Hex.encodeHexString(result);
        final String proof = Hex.encodeHexString(secretBytes);
// Alice is a PUBLIC net user
const alicePrivateKey = process.env.ALICE_PRIVATE_KEY;
const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MAIN_NET);

// Alice picks a random number and hashes it.
const random = crypto.randomBytes(10);
const hash = sha3_512.create();
const secret = hash.update(random).hex().toUpperCase();
const proof = random.toString('hex');

Now, Alice creates a secret lock transaction, which contains:

  • The mosaic and amount to be sent: 10 alice:token
  • A recipient address: Bob’s address in public chain
  • The secret: Hashed proof.
  • The amount of time while funds can be unlocked: 96h
// Alice creates creates TX1 SecretLockTransaction{ H(x), B, MosaicId, Amount, valid for 96h }
const tx1 = SecretLockTransaction.create(
    Deadline.create(),
    new Mosaic(new MosaicId('alice:token'), UInt64.fromUint(10)),
    UInt64.fromUint(96*60), //assume one block per minute
    HashType.SHA3_512,
    secret,
    Address.createFromRawAddress('SDG4WG-FS7EQJ-KFQKXM-4IUCQG-PXUW5H-DJVIJB-OXJG'),
    NetworkType.MAIN_NET
);
        // Alice creates creates TX1 SecretLockTransaction{ H(x), B, MosaicId, Amount, valid for 96h }
        final SecretLockTransaction tx1 = SecretLockTransaction.create(
                new Deadline(2, HOURS),
                new Mosaic(new MosaicId("alice:token"), BigInteger.valueOf(10)),
                BigInteger.valueOf(96 * 60), //assume one block per minute
                HashType.SHA3_512,
                secret,
                Address.createFromRawAddress("NDG4WG-FS7EQJ-KFQKXM-4IUCQG-PXUW5H-DJVIJB-OXJG"),
                NetworkType.MAIN_NET
        );
// Alice creates creates TX1 SecretLockTransaction{ H(x), B, MosaicId, Amount, valid for 96h }
const tx1 = SecretLockTransaction.create(
    Deadline.create(),
    new Mosaic(new MosaicId('alice:token'), UInt64.fromUint(10)),
    UInt64.fromUint(96*60), //assume one block per minute
    HashType.SHA3_512,
    secret,
    Address.createFromRawAddress('SDG4WG-FS7EQJ-KFQKXM-4IUCQG-PXUW5H-DJVIJB-OXJG'),
    NetworkType.MAIN_NET
);

Alice signs and announces TX1 to NEM network.

// Alice sends TX1 to network (PUBLIC)
const tx1Signed = aliceAccount.sign(tx1);
const transactionHttp = new TransactionHttp('http://localhost:3000');
transactionHttp.announce(tx1Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);
        // Alice sends TX1 to network (PUBLIC)
        final SignedTransaction tx1Signed = aliceAccount.sign(tx1);
        final TransactionHttp transactionHttp = new TransactionHttp("http://localhost:3000");
        transactionHttp.announce(tx1Signed).toFuture().get();
// Alice sends TX1 to network (PUBLIC)
const tx1Signed = aliceAccount.sign(tx1);
const transactionHttp = new TransactionHttp('http://localhost:3000');
transactionHttp.announce(tx1Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);

Alice can tell Bob the secret, or he can retrieve it directly from the recently announced transaction.

Bob creates a secret lock transaction TX2, which contains:

  • The mosaic and amount to be sent: 10 bob:token
  • A recipient address: Alice’s address in private chain
  • The secret that should be achieved to unlock the funds.
  • The amount of time while funds can be unlocked: 84h

Note

The amount of time while funds can be unlocked should be smaller time frame than TX1’s. Alice knows the secret, so Bob must be sure, he’ll have some time left after Alice will release the secret.

// Bob is a PRIVATE network user
const bobPrivateKey = process.env.BOB_PRIVATE_KEY as string;
const bobAccount = Account.createFromPrivateKey(bobPrivateKey, NetworkType.MIJIN);


// B creates TX2 SecretLockTransaction{ H(x), A, MosaicId, Amount, valid for 84h }
const tx2 = SecretLockTransaction.create(
    Deadline.create(),
    new Mosaic(new MosaicId('bob:token'), UInt64.fromUint(10)),
    UInt64.fromUint(84*60), //assume one block per minute
    HashType.SHA3_512,
    secret,
    Address.createFromRawAddress('SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54'),
    NetworkType.MIJIN
);
        // Bob is a PRIVATE network user
        final String bobPrivateKey = "";
        final Account bobAccount = Account.createFromPrivateKey(bobPrivateKey, NetworkType.MIJIN);

        // B creates TX2 SecretLockTransaction{ H(x), A, MosaicId, Amount, valid for 84h }
        final SecretLockTransaction tx2 = SecretLockTransaction.create(
                new Deadline(2, HOURS),
                new Mosaic(new MosaicId("bob:token"), BigInteger.valueOf(10)),
                BigInteger.valueOf(84 * 60), //assume one block per minute
                HashType.SHA3_512,
                secret,
                Address.createFromRawAddress("MD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54"),
                NetworkType.MIJIN
        );
// Bob is a PRIVATE network user
const bobPrivateKey = process.env.BOB_PRIVATE_KEY;
const bobAccount = Account.createFromPrivateKey(bobPrivateKey, NetworkType.MIJIN);


// B creates TX2 SecretLockTransaction{ H(x), A, MosaicId, Amount, valid for 84h }
const tx2 = SecretLockTransaction.create(
    Deadline.create(),
    new Mosaic(new MosaicId('bob:token'), UInt64.fromUint(10)),
    UInt64.fromUint(84*60), //assume one block per minute
    HashType.SHA3_512,
    secret,
    Address.createFromRawAddress('SD5DT3-CH4BLA-BL5HIM-EKP2TA-PUKF4N-Y3L5HR-IR54'),
    NetworkType.MIJIN
);

Once signed, Bob announces TX2 to MIJIN network.

// Bob sends TX2 to network (PRIVATE)
const tx2Signed = bobAccount.sign(tx2);
transactionHttp.announce(tx2Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);
        // Bob sends TX2 to network (PRIVATE)
        final SignedTransaction tx2Signed = bobAccount.sign(tx2);
        transactionHttp.announce(tx2Signed).toFuture().get();
// Bob sends TX2 to network (PRIVATE)
const tx2Signed = bobAccount.sign(tx2);
transactionHttp.announce(tx2Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);

Now, Alice can announce a secret proof transaction in MIJIN network, selecting encrypting algorithm, the original proof and secret used.

// Alice waits until Txs are confirmed
// Alice spends TX2 transaction by sending SecretProofTransaction (in PRIVATE network)
const tx3 = SecretProofTransaction.create(
    Deadline.create(),
    HashType.SHA3_512,
    secret,
    proof,
    NetworkType.MIJIN
);

const tx3Signed = aliceAccount.sign(tx3);
transactionHttp.announce(tx3Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);
        // Alice spends TX2 transaction by sending SecretProofTransaction (in PRIVATE network)
        SecretProofTransaction tx3 = SecretProofTransaction.create(
                new Deadline(2, HOURS),
                HashType.SHA3_512,
                secret,
                proof,
                NetworkType.MIJIN
        );

        final SignedTransaction tx3Signed = aliceAccount.sign(tx3);
        transactionHttp.announce(tx3Signed).toFuture().get();
// Alice waits until Txs are confirmed
// Alice spends TX2 transaction by sending SecretProofTransaction (in PRIVATE network)
const tx3 = SecretProofTransaction.create(
    Deadline.create(),
    HashType.SHA3_512,
    secret,
    proof,
    NetworkType.MIJIN
);

const tx3Signed = aliceAccount.sign(tx3);
transactionHttp.announce(tx3Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);

If all goes well, Alice unlocks TX2 funds, and the proof is revealed. Bob does the same by announcing a secret proof transaction TX4 in NEM blockchain.

// Bob spends TX1 transaction by sending SecretProofTransaction (in PUBLIC network)
const tx4 = SecretProofTransaction.create(
    Deadline.create(),
    HashType.SHA3_512,
    secret,
    proof,
    NetworkType.MAIN_NET
);
const tx4Signed = aliceAccount.sign(tx4);

transactionHttp.announce(tx4Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);
        // Bob spends TX1 transaction by sending SecretProofTransaction (in PUBLIC network)
        SecretProofTransaction tx4 = SecretProofTransaction.create(
                new Deadline(2, HOURS),
                HashType.SHA3_512,
                secret,
                proof,
                NetworkType.MAIN_NET
        );


        final SignedTransaction tx4Signed = aliceAccount.sign(tx4);
        transactionHttp.announce(tx4Signed).toFuture().get();
    }
}
// Bob spends TX1 transaction by sending SecretProofTransaction (in PUBLIC network)
const tx4 = SecretProofTransaction.create(
    Deadline.create(),
    HashType.SHA3_512,
    secret,
    proof,
    NetworkType.MAIN_NET
);
const tx4Signed = aliceAccount.sign(tx4);

transactionHttp.announce(tx4Signed).subscribe(
    x => console.log(x),
    err => console.error(err)
);

At that moment, Bob unlocks TX1 funds, and the atomic cross-chain swap concludes.

Is it atomic?

Consider the following scenarios:

  1. Bob doesn’t want to announce Tx2. Alice will receive his funds back after 94 hours.
  2. Alice does not want to swap tokens by signing Tx3. Bob will receive his refund after 84h. Alice will unlock as well her funds after 94 hours.
  3. Alice signs and announces Tx3, receiving Bob’s funds. Bob will have time to sign Tx4, as Tx1 validity is longer than Tx2.

The process is atomic but should be completed with lots of time before the deadlines.