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 Bitxor

Normalizing the previous description into Bitxor 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: Bitxor 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 tokens

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

Owner

Amount

TokenId

Description

Alice

100

bitxor

Native currency token

Ticket distributor

1

7cdf3b117a3c40cc

Represents a museum ticket.

Before continuing, create the two accounts loaded with bitxor. You should also create a token with the ticket distributor’s account. This new token 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 bitxor.

  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 token 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 token id
const ticketTokenId = new TokenId('7cdf3b117a3c40cc');
// replace with ticket token id divisibility
const ticketDivisibility = 0;
// replace with bitxor id
const networkCurrencyTokenId = new TokenId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;

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

const ticketDistributorToAliceTx = TransferTransaction.create(
  Deadline.create(epochAdjustment),
  aliceAccount.address,
  [
    new Token(
      ticketTokenId,
      UInt64.fromUint(1 * Math.pow(10, ticketDivisibility)),
    ),
  ],
  PlainMessage.create('send 1 museum ticket to customer'),
  networkType,
);
// replace with network type
const networkType = bitxor_sdk_1.NetworkType.TEST_NET;
// replace with alice private key
const alicePrivateKey =
    '1111111111111111111111111111111111111111111111111111111111111111';
const aliceAccount = bitxor_sdk_1.Account.createFromPrivateKey(
    alicePrivateKey,
    networkType,
);
// replace with ticket distributor public key
const ticketDistributorPublicKey =
    '20330294DC18D96BDEEF32FB02338A6462A0469CB451A081DE2F05B4302C0C0A';
const ticketDistributorPublicAccount = bitxor_sdk_1.PublicAccount.createFromPublicKey(
    ticketDistributorPublicKey,
    networkType,
);
// replace with ticket token id
const ticketTokenId = new bitxor_sdk_1.TokenId('7cdf3b117a3c40cc');
// replace with ticket token id divisibility
const ticketDivisibility = 0;
// replace with bitxor id
const networkCurrencyTokenId = new bitxor_sdk_1.TokenId('5E62990DCAC5BE8A');
// replace with network currency divisibility
const networkCurrencyDivisibility = 6;
const aliceToTicketDistributorTx = bitxor_sdk_1.TransferTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    ticketDistributorPublicAccount.address, [
        new bitxor_sdk_1.Token(
            networkCurrencyTokenId,
            bitxor_sdk_1.UInt64.fromUint(
                100 * Math.pow(10, networkCurrencyDivisibility),
            ),
        ),
    ],
    bitxor_sdk_1.PlainMessage.create('send 100 bitxor to distributor'),
    networkType,
);
const ticketDistributorToAliceTx = bitxor_sdk_1.TransferTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    aliceAccount.address, [
        new bitxor_sdk_1.Token(
            ticketTokenId,
            bitxor_sdk_1.UInt64.fromUint(1 * Math.pow(10, ticketDivisibility)),
        ),
    ],
    bitxor_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 token id
            TokenId ticketTokenId = new TokenId("7cdf3b117a3c40cc");
            int ticketDivisibility = 0;
            NetworkCurrency ticketCurrency = new NetworkCurrencyBuilder(ticketTokenId, ticketDivisibility).build();
            // replace with ticket token 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 bitxor 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 = bitxor_sdk_1.AggregateTransaction.createBonded(
    bitxor_sdk_1.Deadline.create(epochAdjustment), [
        aliceToTicketDistributorTx.toAggregate(aliceAccount.publicAccount),
        ticketDistributorToAliceTx.toAggregate(ticketDistributorPublicAccount),
    ],
    networkType, [],
    bitxor_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 bitxor to prevent spamming the network. Once the ticket distributor signs the AggregateTransaction, the amount of locked bitxor becomes available again on Alice’s account, and the exchange will get through.

const hashLockTransaction = HashLockTransaction.create(
  Deadline.create(epochAdjustment),
  new Token(
    networkCurrencyTokenId,
    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 = bitxor_sdk_1.HashLockTransaction.create(
    bitxor_sdk_1.Deadline.create(epochAdjustment),
    new bitxor_sdk_1.Token(
        networkCurrencyTokenId,
        bitxor_sdk_1.UInt64.fromUint(
            10 * Math.pow(10, networkCurrencyDivisibility),
        ),
    ),
    bitxor_sdk_1.UInt64.fromUint(480),
    signedTransaction,
    networkType,
    bitxor_sdk_1.UInt64.fromUint(2000000),
);
const signedHashLockTransaction = aliceAccount.sign(
    hashLockTransaction,
    networkGenerationHash,
);
// replace with node endpoint
const nodeUrl = 'NODE_URL';
const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(nodeUrl);
const listener = repositoryFactory.createListener();
const receiptHttp = repositoryFactory.createReceiptRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionService = new bitxor_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.