Creating an escrow contract

Learn about aggregate bonded transactions creating an escrow contract.

Background

An escrow is a contractual arrangement in which a third party receives and disburses money or documents for the primary transacting parties. This disbursement is dependent on the conditions agreed 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. See the full description at Wikipedia.

For this example, imagine that the two parties agree on a virtual service, implying that the escrow can be executed immediately:

  1. The buyer and seller agree on terms.
  2. The buyer submits payment to escrow.
  3. The seller delivers goods or service to the buyer.
  4. The buyer approves goods or service.
  5. The escrow releases payment to the seller.

How to create an escrow contract with NEM

Normalizing the previous description into NEM related concepts:

  • contractual arrangement: A new type of transaction called AggregateTransaction.
  • third party receives and disburses money: There is no third party, we are going to use blockchain technology.
  • primary transacting parties: NEM accounts will represent the participants.
  • conditions agreed to by the transacting parties: When every participant signs the AggregateTransaction.
  • account established by a broker for holding funds: There will not be an intermediate account, the exchange will happen atomically using an AggregateTransaction.
  • until the consummation or termination of a transaction: The transaction gets included in a block or expires.

Getting into some code

../../_images/aggregate-escrow-11.png

Multi-Asset Escrowed Transactions

Setting up the required accounts and mosaics

Alice and a ticket distributor want to swap the following mosaics.

Owner Amount MosaicId Description
Alice 100 cat.currency Native currency mosaic
Ticket distributor 1 7cdf3b117a3c40cc Represents a museum ticket.

Before continuing, create the two accounts loaded with cat.currency. You should also create a mosaic with the ticket distributor’s account. This new mosaic will represent the ticket.

Creating the escrow contract

  1. Open a new file, and define two transfer transactions:
  1. A TransferTransaction from Alice to the ticket distributor sending 100 cat.currency.
  2. A TransferTransaction from the ticket distributor to Alice sending 1 7cdf3b117a3c40cc (museum ticket).

Note

The museum ticket does not have the id 7cdf3b117a3c40cc in your network. Replace the mosaic identifier for the one you have created in the previous step.

const alicePrivateKey = process.env.ALICE_PRIVATE_KEY as string;
const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MIJIN_TEST);

const ticketDistributorPublicKey = process.env.TICKET_DISTRIBUTOR_PUBLIC_KEY as string;
const ticketDistributorPublicAccount = PublicAccount.createFromPublicKey(ticketDistributorPublicKey, NetworkType.MIJIN_TEST);

const aliceToTicketDistributorTx = TransferTransaction.create(
    Deadline.create(),
    ticketDistributorPublicAccount.address,
    [NetworkCurrencyMosaic.createRelative(100)],
    PlainMessage.create('send 100 cat.currency to distributor'),
    NetworkType.MIJIN_TEST);

const ticketDistributorToAliceTx = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [new Mosaic(new MosaicId('7cdf3b117a3c40cc'), UInt64.fromUint(1))],
    PlainMessage.create('send 1 museum ticket to alice'),
    NetworkType.MIJIN_TEST);
const alicePrivateKey = process.env.ALICE_PRIVATE_KEY;
const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, NetworkType.MIJIN_TEST);

const ticketDistributorPublicKey = process.env.TICKET_DISTRIBUTOR_PUBLIC_KEY;
const ticketDistributorPublicAccount = PublicAccount.createFromPublicKey(ticketDistributorPublicKey, NetworkType.MIJIN_TEST);

const aliceToTicketDistributorTx = TransferTransaction.create(
    Deadline.create(),
    ticketDistributorPublicAccount.address,
    [NetworkCurrencyMosaic.createRelative(100)],
    PlainMessage.create('send 100 cat.currency to distributor'),
    NetworkType.MIJIN_TEST);

const ticketDistributorToAliceTx = TransferTransaction.create(
    Deadline.create(),
    aliceAccount.address,
    [new Mosaic(new MosaicId('7cdf3b117a3c40cc'), UInt64.fromUint(1))],
    PlainMessage.create('send 1 museum ticket to alice'),
    NetworkType.MIJIN_TEST);
  1. Wrap the defined transactions in an AggregateTransaction and sign it with Alice’s account. An AggregateTransaction 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 signatures are required from other participants—the ticket distributor—it is considered bonded.
const aggregateTransaction = AggregateTransaction.createBonded(Deadline.create(),
    [aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
        ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount)],
    NetworkType.MIJIN_TEST);

const networkGenerationHash = process.env.NETWORK_GENERATION_HASH as string;
const signedTransaction = aliceAccount.sign(aggregateTransaction, networkGenerationHash);
console.log("Aggregate Transaction Hash: " + signedTransaction.hash);
const aggregateTransaction = AggregateTransaction.createBonded(Deadline.create(),
    [aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
        ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount)],
    NetworkType.MIJIN_TEST);

const networkGenerationHash = process.env.NETWORK_GENERATION_HASH;
const signedTransaction = aliceAccount.sign(aggregateTransaction, networkGenerationHash);
console.log("Aggregate Transaction Hash: " + signedTransaction.hash);
  1. When an AggregateTransaction is bonded, Alice will need to lock 10 cat.currency to prevent spamming the network. Once the ticket distributor signs the AggregateTransaction, the amount of locked cat.currency becomes available again on Alice’s account, and the exchange will get through.
const hashLockTransaction = HashLockTransaction.create(
    Deadline.create(),
    NetworkCurrencyMosaic.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const signedHashLockTransaction = aliceAccount.sign(hashLockTransaction, networkGenerationHash);

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

const announceHashLockTransaction = (signedHashLockTransaction: SignedTransaction) => {
    return transactionHttp.announce(signedHashLockTransaction);
};

const announceAggregateTransaction = (listener: Listener,
                                      signedHashLockTransaction: SignedTransaction,
                                      signedAggregateTransaction: SignedTransaction,
                                      senderAddress: Address) => {
    return listener
        .confirmed(senderAddress)
        .pipe(
            filter((transaction) => transaction.transactionInfo !== undefined
                && transaction.transactionInfo.hash === signedHashLockTransaction.hash),
            mergeMap(ignored => {
                listener.terminate();
                return transactionHttp.announceAggregateBonded(signedAggregateTransaction)
            })
        );
};

listener.open().then(() => {
    merge(announceHashLockTransaction(signedHashLockTransaction),
        announceAggregateTransaction(listener, signedHashLockTransaction, signedTransaction, aliceAccount.address))
        .subscribe(x => console.log('Transaction confirmed:', x.message),
            err=> console.log(err));
});
const hashLockTransaction = HashLockTransaction.create(
    Deadline.create(),
    NetworkCurrencyMosaic.createRelative(10),
    UInt64.fromUint(480),
    signedTransaction,
    NetworkType.MIJIN_TEST);

const signedHashLockTransaction = aliceAccount.sign(hashLockTransaction, networkGenerationHash);

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

const announceHashLockTransaction = (signedHashLockTransaction) => {
    return transactionHttp.announce(signedHashLockTransaction);
};

const announceAggregateTransaction = (listener,
                                      signedHashLockTransaction,
                                      signedAggregateTransaction,
                                      senderAddress) => {
    return listener
        .confirmed(senderAddress)
        .pipe(
            filter((transaction) => transaction.transactionInfo !== undefined
                && transaction.transactionInfo.hash === signedHashLockTransaction.hash),
            mergeMap(ignored => {
                listener.terminate();
                return transactionHttp.announceAggregateBonded(signedAggregateTransaction)
            })
        );
};

listener.open().then(() => {
    merge(announceHashLockTransaction(signedHashLockTransaction),
        announceAggregateTransaction(listener, signedHashLockTransaction, signedTransaction, aliceAccount.address))
        .subscribe(x => console.log('Transaction confirmed:', x.message),
            err=> console.log(err));
});

The distributor has not signed the AggregateBondedTransaction yet, so the exchange has not been completed.

  1. Copy the AggregateTransaction hash from (2), and check how to cosign the AggregateTransaction following the next guide.

Is it possible without aggregate transactions?

It is not secure, since:

  • Alice could decide not to pay the distributor after receiving the ticket.
  • The distributor could choose not to send the ticket after receiving the payment.

Using the AggregateTransaction feature we ensure that multiple transactions are executed at the same time when all the participants agree.

What’s next?

Try to swap mosaics adding a third participant.

../../_images/aggregate-escrow-2.png

Multi-Asset Escrowed Transactions