Creating an escrow contract

Learn about aggregate bonded transactions creating an escrow contract.

Use case

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.

../../_images/aggregate-escrow-1.png

Multi-Asset Escrowed Transactions

How to create an escrow contract with Symbol

Normalizing the previous description into Symbol related concepts:

  • contractual arrangement: A new type of transaction called Aggregate Transaction.

  • third party receives and disburses money: There is no third party, we are going to use blockchain technology.

  • primary transacting parties: Symbol 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.

Prerequisites

Setting up the required accounts and mosaics

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

Owner

Amount

MosaicId

Description

Alice

100

symbol.xym

Native currency mosaic

Ticket distributor

1

7cdf3b117a3c40cc

Represents a museum ticket.

Before continuing, create the two accounts loaded with symbol.xym. 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 symbol.xym.

  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.

// replace with network type
const networkType = NetworkType.TEST_NET;
// replace with alice private key
const alicePrivateKey =
  '1111111111111111111111111111111111111111111111111111111111111111';
const aliceAccount = Account.createFromPrivateKey(alicePrivateKey, networkType);
// replace with ticket distributor public key
const ticketDistributorPublicKey =
  '20330294DC18D96BDEEF32FB02338A6462A0469CB451A081DE2F05B4302C0C0A';
const ticketDistributorPublicAccount = PublicAccount.createFromPublicKey(
  ticketDistributorPublicKey,
  networkType,
);
// replace with ticket mosaic id
const ticketMosaicId = new MosaicId('7cdf3b117a3c40cc');
// replace with ticket mosaic id divisibility
const ticketDivisibility = 0;
// replace with symbol.xym id
const networkCurrencyMosaicId = new MosaicId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;

const aliceToTicketDistributorTx = TransferTransaction.create(
  Deadline.create(epochAdjustment),
  ticketDistributorPublicAccount.address,
  [
    new Mosaic(
      networkCurrencyMosaicId,
      UInt64.fromUint(100 * Math.pow(10, networkCurrencyDivisibility)),
    ),
  ],
  PlainMessage.create('send 100 symbol.xym to distributor'),
  networkType,
);

const ticketDistributorToAliceTx = TransferTransaction.create(
  Deadline.create(epochAdjustment),
  aliceAccount.address,
  [
    new Mosaic(
      ticketMosaicId,
      UInt64.fromUint(1 * Math.pow(10, ticketDivisibility)),
    ),
  ],
  PlainMessage.create('send 1 museum ticket to customer'),
  networkType,
);
// replace with network type
const networkType = symbol_sdk_1.NetworkType.TEST_NET;
// replace with alice private key
const alicePrivateKey =
  '1111111111111111111111111111111111111111111111111111111111111111';
const aliceAccount = symbol_sdk_1.Account.createFromPrivateKey(
  alicePrivateKey,
  networkType,
);
// replace with ticket distributor public key
const ticketDistributorPublicKey =
  '20330294DC18D96BDEEF32FB02338A6462A0469CB451A081DE2F05B4302C0C0A';
const ticketDistributorPublicAccount = symbol_sdk_1.PublicAccount.createFromPublicKey(
  ticketDistributorPublicKey,
  networkType,
);
// replace with ticket mosaic id
const ticketMosaicId = new symbol_sdk_1.MosaicId('7cdf3b117a3c40cc');
// replace with ticket mosaic id divisibility
const ticketDivisibility = 0;
// replace with symbol.xym id
const networkCurrencyMosaicId = new symbol_sdk_1.MosaicId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;
const aliceToTicketDistributorTx = symbol_sdk_1.TransferTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  ticketDistributorPublicAccount.address,
  [
    new symbol_sdk_1.Mosaic(
      networkCurrencyMosaicId,
      symbol_sdk_1.UInt64.fromUint(
        100 * Math.pow(10, networkCurrencyDivisibility),
      ),
    ),
  ],
  symbol_sdk_1.PlainMessage.create('send 100 symbol.xym to distributor'),
  networkType,
);
const ticketDistributorToAliceTx = symbol_sdk_1.TransferTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  aliceAccount.address,
  [
    new symbol_sdk_1.Mosaic(
      ticketMosaicId,
      symbol_sdk_1.UInt64.fromUint(1 * Math.pow(10, ticketDivisibility)),
    ),
  ],
  symbol_sdk_1.PlainMessage.create('send 1 museum ticket to customer'),
  networkType,
);
            NetworkType networkType = repositoryFactory.getNetworkType().toFuture().get();

            // replace with alice private key
            String alicePrivatekey = "";
            Account aliceAccount = Account.createFromPrivateKey(alicePrivatekey, networkType);

            // replace with bob public key
            String ticketDistributorPublicKey = "";
            PublicAccount ticketDistributorPublicAccount = PublicAccount
                .createFromPublicKey(ticketDistributorPublicKey, networkType);

            // replace with ticket mosaic id
            MosaicId ticketMosaicId = new MosaicId("7cdf3b117a3c40cc");
            int ticketDivisibility = 0;
            NetworkCurrency ticketCurrency = new NetworkCurrencyBuilder(ticketMosaicId, ticketDivisibility).build();
            // replace with ticket mosaic id divisibility
            NetworkCurrency networkCurrency = repositoryFactory.getNetworkCurrency().toFuture().get();

            TransferTransaction aliceToTicketDistributorTx = TransferTransactionFactory
                .create(networkType, ticketDistributorPublicAccount.getAddress(),
                    Collections.singletonList(networkCurrency.createRelative(BigInteger.valueOf(100))),
                    PlainMessage.create("send 100 symbol.xym to distributor")).build();

            TransferTransaction ticketDistributorToAliceTx = TransferTransactionFactory
                .create(networkType, aliceAccount.getAddress(),
                    Collections.singletonList(ticketCurrency.createRelative(BigInteger.ONE)),
                    PlainMessage.create("send 1 museum ticket to customer")).build();

2. Wrap the defined transactions in an Aggregate Transaction 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(epochAdjustment),
  [
    aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
    ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount),
  ],
  networkType,
  [],
  UInt64.fromUint(2000000),
);

// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = aliceAccount.sign(
  aggregateTransaction,
  networkGenerationHash,
);
console.log('Aggregate Transaction Hash:', signedTransaction.hash);
const aggregateTransaction = symbol_sdk_1.AggregateTransaction.createBonded(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  [
    aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
    ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount),
  ],
  networkType,
  [],
  symbol_sdk_1.UInt64.fromUint(2000000),
);
// replace with meta.networkGenerationHash (nodeUrl + '/node/info')
const networkGenerationHash =
  '1DFB2FAA9E7F054168B0C5FCB84F4DEB62CC2B4D317D861F3168D161F54EA78B';
const signedTransaction = aliceAccount.sign(
  aggregateTransaction,
  networkGenerationHash,
);
console.log('Aggregate Transaction Hash:', signedTransaction.hash);
            AggregateTransaction aggregateTransaction = AggregateTransactionFactory.createBonded(networkType, Arrays
                .asList(aliceToTicketDistributorTx.toAggregate(aliceAccount.getPublicAccount()),
                    ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount)))
                .maxFee(BigInteger.valueOf(2000000)).build();

            String generationHash = repositoryFactory.getGenerationHash().toFuture().get();
            SignedTransaction signedTransaction = aliceAccount.sign(aggregateTransaction, generationHash);

3. When an AggregateTransaction is bonded, Alice will need to lock 10 symbol.xym to prevent spamming the network. Once the ticket distributor signs the AggregateTransaction, the amount of locked symbol.xym becomes available again on Alice’s account, and the exchange will get through.

const hashLockTransaction = HashLockTransaction.create(
  Deadline.create(epochAdjustment),
  new Mosaic(
    networkCurrencyMosaicId,
    UInt64.fromUint(10 * Math.pow(10, networkCurrencyDivisibility)),
  ),
  UInt64.fromUint(480),
  signedTransaction,
  networkType,
  UInt64.fromUint(2000000),
);

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

// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new TransactionService(transactionHttp, receiptHttp);

listener.open().then(() => {
  transactionService
    .announceHashLockAggregateBonded(
      signedHashLockTransaction,
      signedTransaction,
      listener,
    )
    .subscribe(
      (x) => console.log(x),
      (err) => console.log(err),
      () => listener.close(),
    );
});
const hashLockTransaction = symbol_sdk_1.HashLockTransaction.create(
  symbol_sdk_1.Deadline.create(epochAdjustment),
  new symbol_sdk_1.Mosaic(
    networkCurrencyMosaicId,
    symbol_sdk_1.UInt64.fromUint(
      10 * Math.pow(10, networkCurrencyDivisibility),
    ),
  ),
  symbol_sdk_1.UInt64.fromUint(480),
  signedTransaction,
  networkType,
  symbol_sdk_1.UInt64.fromUint(2000000),
);
const signedHashLockTransaction = aliceAccount.sign(
  hashLockTransaction,
  networkGenerationHash,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new symbol_sdk_1.TransactionService(
  transactionHttp,
  receiptHttp,
);
listener.open().then(() => {
  transactionService
    .announceHashLockAggregateBonded(
      signedHashLockTransaction,
      signedTransaction,
      listener,
    )
    .subscribe(
      (x) => console.log(x),
      (err) => console.log(err),
      () => listener.close(),
    );
});
            HashLockTransaction hashLockTransaction = HashLockTransactionFactory
                .create(networkType, networkCurrency.createRelative(BigDecimal.valueOf(10)), BigInteger.valueOf(480),
                    signedTransaction).build();

            SignedTransaction signedHashLockTransaction = aliceAccount.sign(hashLockTransaction, generationHash);
            try (Listener listener = repositoryFactory.createListener()) {
                listener.open().get();
                TransactionService transactionService = new TransactionServiceImpl(repositoryFactory);

                transactionService.announceHashLockAggregateBonded(listener, signedHashLockTransaction, signedTransaction).toFuture()
                    .get();
            }

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 by 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.