Complex Asset Transfers

Overview

Inside each address, there are two parts associated with assets as part of their state:

  1. A public key: to control access of the assets
  2. A balance: consisting of multiple boxes, the sum of the amounts contained in those boxes is the number of assets in this address

Assets in an address are transferred by signing using the private key corresponding to the public key as we have explored in the first asset tutorial.

Split Boxes

One asset box can be split into multiple asset boxes. This is mostly used to send assets to others while returning the change. Below is an example of splitting it into two. The total number of assets are conserved after the split i.e. x0 + x1 + y0 + y1 = x + y

12701270

The relevant code is listed below with self-explanatory comments inline

const BramblJS = require("brambljs);

/**
    * Sends a signed transaction
  *
  */
async function sendSignedTransaction(signedTransactionData, brambljs) {
    let obj = {};
    return new Promise((resolve, reject) => {
        let e = brambljs.requests
            .broadcastTx({ tx: signedTransactionData })
            .then(function (result) {
                obj.txId = result.result.txId;
                obj.result = result.result;
                resolve(obj);
            })
            .catch(function (err) {
                console.error(err);
                obj.error = err.message;
                reject(err);
            });
        return e;
    });
}

/**
     * Sign transaction with tx object and private key
     * @param {Object} txObject is the transaction object
     * @return {(Promise|Object)} signed object with rawTransaction data to be uused for sending transaction
     */
    async function signTransaction(txObject, brambljs) {
        let obj = {};
        return await brambljs.addSigToTx(txObject.messageToSign.result, txObject.keys).catch(function (err) {
            console.error(err);
            obj.error = err.message;
            return obj;
        });
    }

/**
    * First signs a transaction, then sends the signedTransaction to the network to be confirmed into a block
  *
  */
async function signAndSendTransaction(txObject, brambljs) {
    let obj = {};
    return signTransaction(txObject, brambljs).then(function (value) {
        if (value.error) {
            obj.error = value.error;
            return obj;
        } else {
            return sendSignedTransaction(value, brambljs).then(function (value) {
                if (value.error) {
                    obj.error = value.error;
                    return obj;
                } else {
                    return value;
                }
            });
        }
    });
}

 /**
     * Gets the raw transaction object on the asset transaction you plan on signing before sending.
     * Allows verification of the asset transaction is correct as well as providing the message to sign
     * @param {Object} txObject is the req.body from the service that has passed validation
     * @return {Object} is the completed object that contains data about poly transactions and the message to sign.
     */
    async function sendRawAssetTransaction(txObject, brambljs) {
        let obj = {};
        const formattedRecipients = [];
        obj.keys = [txObject.senderKeyManager, txObject.issuerKeyManager]
        const params = {
            propositionType: txObject.propositionType,
            recipients: txObject.recipients,
            fee: txObject.fee,
            sender: [txObject.senderKeyManager.address],
            changeAddress: txObject.changeAddress,
            data: txObject.data,
            consolidationAddress: txObject.consolidationAddress,
            minting: txObject.minting,
            assetCode: txObject.assetCode,
        };
        return brambljs.requests
            .createRawAssetTransfer(params)
            .then(function (result) {
                obj.messageToSign = result;
                return obj;
            })
            .catch(function (err) {
                console.error(err);
                obj.err = err.message;
                return obj;
            });
    }

async function assetTransaction(txObject, brambljs) {
    // first create the rawAssetTransfer transaction
    return sendRawAssetTransaction(txObject, brambljs).then(function(rpcResponse) {
        if (rpcResponse.err) {
            throw Error(rpcResponse.err);
        }
        // then sign and send the transaction
        return signAndSendTransaction(rpcResponse, brambljs)
    });
}

async function split(senderKeyManager,assetIssuerKeyManager, recipientAddress, assetAmount, consolidationKeyManager, consolidationAmount, fee, securityRoot, metadata, assetCode, brambljs) {
   
  // send assetAmount of your asset to the recipient, then consolidationAmount to the consolidationAddress
  const txObject = {
        senderKeyManager: senderKeyManager,
        issuerKeyManager: assetIssuerKeyManager,
        recipients: [[
            recipientAddress,
            assetAmount,
            securityRoot,
            metadata
        ]],
        propositionType: "PublicKeyCurve25519",
        changeAddress: senderKeyManager.address,
        consolidationAddress: consolidationKeyManager.address,
        minting: false,
        fee: fee,
        assetCode: assetCode
    }

    const pollParams = {
        "timeout": 90,
        "interval": 3,
        "maxFailedQueries": 10
    };

    // first create the rawAssetTransfer transaction
    await assetTransaction(txObject, brambljs).then(res => brambljs.pollTx(res.txId, pollParams));

    txObject.senderKeyManager = consolidationKeyManager
    txObject.recipients = [
        [
            consolidationKeyManager.address,
            consolidationAmount,
            securityRoot,
            metadata
        ]]
    txObject.changeAddress = consolidationKeyManager.address

    txObject.consolidationAddress = consolidationKeyManager.address
  
  // second, update the securityRoot and metadata on the assets that were sent to the consolidationAddress

    return await assetTransaction(txObject, brambljs).then(res => brambljs.pollTx(res.txId, pollParams));
}

// Above are helper functions, main js logic here
// please replace "yourProjectId" with your project id
const networkUrl = "https://vertx.topl.services/valhalla/{{yourProjectId}}";
// please replace "yourApiKey" with your apiKey
const apiKey = "yourApiKey";

const password = "test";
const networkPrefix = "valhalla";

// please replace with your encrypted keyfile and password
const recipientKeyManager = BramblJS.KeyManager.importKeyFileFromDisk("src/valhalla_keyfiles/your_valhalla_key_file", "yourPassword")

const brambljs = new BramblJS({
    networkPrefix: networkPrefix,
    password: password,
    Requests: {
        url: `${networkUrl}`,
        apiKey: `${apiKey}`,
    },
    KeyManager: recipientKeyManager
});

const assetCode = brambljs.createAssetCode("covfefe");


const txObject = {
    senderKeyManager: brambljs.keyManager,
    issuerKeyManager: brambljs.keyManager,
    recipients: [[
        brambljs.keyManager.address,
        10
    ]],
    propositionType: "PublicKeyCurve25519",
    changeAddress: brambljs.keyManager.address,
    consolidationAddress: brambljs.keyManager.address,
    minting: true,
    fee: 100,
    assetCode: assetCode
}

const pollParams = {
    "timeout": 90,
    "interval": 3,
    "maxFailedQueries": 10
};

const securityRoot = "11111111111111111111111111111111"

const metadata = "metadata"

assetTransaction(txObject, brambljs).then(res => brambljs.pollTx(res.txId, pollParams)).then(res => console.log(split(brambljs.keyManager, brambljs.keyManager, "3NK4UGDB1b1E9eYfPtRKwpMXoSCQFctwq2ptkwxjCRDHgxK9nt4P", 5, brambljs.keyManager, 5,100,
    securityRoot, metadata, assetCode, brambljs
    )).then(function(result) {
    return result;
    });

Demo

Above we have the code to generate the asset, then split the assets.

Step 1: Issue Polys

  • Polys need to be added to the sending account of the transaction, there will need to be at least 300 nanopolys in your address in order for the code above to work properly. You can find out how to load your address with polys at the Adding Polys page

Step 2: Issue Assets

  • 10 Assets are issued in the code above

Step 3: Split Asset Box

  • The Asset Box of 10 assets are split into two asset boxes: one containing 5 assets, and the other box containing 5 assets. Those boxes are owned by the recipientAddress and the consolidationAddress respectively

Step 4: Make sure that the securityRoot and metadata are attached to the box owned by the consolidationAddress

  • By default, there is no securityRoot and metadata attached to the boxes that are sent to the consolidationAddress. Thus, we need to make another transaction to add that information back to the box.