Developer Guide

The code examples listed below show you how to make use of the most common features of Liquid. Each example uses the command line and the Liquid Client application (elements-cli) to issue commands to either the Liquid daemon (elementsd) or Liquid Core GUI (elements-qt), depending on which you are running.

The example code can be modified to work in other languages like Python, C#, Ruby, Go, Perl, and Java by following the steps in the App Examples section and issuing the appropriate RPC commands to Liquid using your chosen language instead of via elements-cli.

It is also worth noting that any examples on the Elements website will also work on Liquid, as Liquid is built using the Elements codebase. The Elements website also includes instructions on how to run all the basic and more advanced code examples listed here (and more) from within one file, that you can copy and paste and run from the terminal.


Basic Commands

The Liquid Client (elements-cli) application allows you to issue Remote Procedure Call (RPC) commands to the Liquid Daemon (elementsd) or Liquid Core GUI (elements-qt) from the terminal.

A full list of all the commands you can issue over RPC can be be seen by running:

elements-cli help

To see further information on how to use a certain command you can append the name of the command like this:

elements-cli help sendtoaddress

This returns examples of how to use the command, a list of arguments that can be passed to it and and the format of the results returned.

If you run the above code you will see that the sendtoaddress accepts 2 mandatory arguments and has 8 additional optional arguments.

For example, to use the sendtoaddress command to send an amount of 1 L-BTC and subtract the fee from the amount being sent we would run:

elements-cli sendtoaddress AzppUfWkYpjiThupf2t6Kn1yPTgseg3buBexuMSbZxpPWAsQcucHXFpr4HHGFQAiyBiddvcjAYyoVeMD 1 "" "" true

The result returned from sendtoaddress is the transaction id. For example:

466c7da2555f0e1e8eff0188cbf7ba0e44fae03a61b81a1226c34ed785e4bb58

The result was returned as a string value. Many results are returned as JSON formatted data however. Using getwalletinfo as an example:

elements-cli getwalletinfo

This returns JSON formatted data similar to that below:

{
  "walletname": "",
  "walletversion": 169900,
  "balance": {
    "bitcoin": 100.00000000,
    },
  "unconfirmed_balance": {
    "bitcoin": 0.00000000
  },
  "immature_balance": {
    "bitcoin": 0.00000000
  },
  "txcount": 10,
  "keypoololdest": 1557907592,
  "keypoolsize": 1000,
  "keypoolsize_hd_internal": 999,
  "paytxfee": 0.00000000,
  "hdseedid": "17cea58f08ed56e975216699061df4a75002fcf8",
  "hdmasterkeyid": "17cea58f08ed56e975216699061df4a75002fcf8",
  "private_keys_enabled": true
}

In the remaining code examples we will continue using the terminal and the elements-cli application to send commands to elementsd. Other languages can be used to send RPC commands as mentioned previously.


Confidential Transactions

All addresses in Liquid are, by default, blinded using Confidential Transactions. Blinding is the process by which the amount and type of asset being transferred is cryptographically hidden from everyone except the sending and receiving parties. This is done using a blinding key, which we will look at later.

Imagine that our cryptographic friends Alice and Bob are running Liquid nodes. We’ll have Bob send some assets to himself using a blinded Liquid address as the destination.

First Bob will generate a new address and store it in a variable named “ADDR” so he can recall it for use later:

Bob:~$ ADDR=$(elements-cli getnewaddress)

To see the new address Bob created he can print out the value that was stored in the “ADDR” variable.

Bob:~$ echo $ADDR

Tip

We will use the “store the value returned from an RPC command in a variable for future use” technique throughout the rest of the code examples. We’ll print out the contents of a variable every now and again when highlighting something that has been returned. You can always just echo out the contents of any others as we go along should you want to.

After running the echo command above you should see something similar to this:

Azpr7BwzjwdiB1pNZKcLkk6Esn5NWAE7wtrC4UzEsshpKe3eUZzPQBvfJ7q9wzJLbt9yn8hYZmZDayGG

Tip

As of Liquid v0.17, getnewaddress defaults to creating P2SH-P2WPKH addresses. You can create “CTE” prefixed addresses (the default for versions of Liquid previous to 0.17) by calling getnewaddress like this: elements-cli getnewaddress "" legacy. You can also set the addresstype=legacy argument on node startup, or set it in your config file to always get legacy addresses from “getnewaddress”. Some commands require a “legacy” style address in order to work, such as message signing.

Let’s look at the address in more detail to check that it is indeed a confidential one. To do this we can use the “getaddressinfo” command, passing in the address that we stored in the ADDR variable as a parameter:

Bob:~$ getaddressinfo $ADDR

You should see a long value for the “confidential_key” property within the JSON formatted results. It will look something like this:

"confidential_key": "030788da8d9ca229cbe57e346daaf8d94cba3ed548b41922a8abefaec91ff1abb1"

The confidential_key is the public blinding key, which has been added to the address and is the reason why a confdential address is so long. You will also see that the “getaddressinfo” command shows an associated “unconfidential” address, which can be used to receive assets if you don’t want to make use of the Confidential Transaction feature for some reason.

We’ll now send an amount of 1 “bitcoin” (L-BTC) from Bob’s wallet to the new address we generated for him:

Bob:~$ TXID=$(elements-cli sendtoaddress $ADDR 1)

In order to have the transaction confirm we need a block to be generated. As an aside, at this stage we can query the mempool of each of our Liquid nodes to see the transaction waiting to be added to a block, as well as the current block count of each node’s blockchain:

Alice:~$ elements-cli getrawmempool
Bob:~$ elements-cli getrawmempool
Alice:~$ elements-cli getblockcount
Bob:~$ elements-cli getblockcount

Both should display results including the transaction with the same ID as that stored in the “TXID” variable and the same block count value. Once a new block has been created, we can check the mempool again for each client to see that the transaction has been confirmed:

Bob:~$ elements-cli generatetoaddress 1 $(elements-cli getnewaddress)
Alice:~$ elements-cli getrawmempool
Bob:~$ elements-cli getrawmempool
Alice:~$ elements-cli getblockcount
Bob:~$ elements-cli getblockcount

Note that although Bob sent an amount of 1 L-BTC to himself the net effect is that he now has slightly less than he did before, this is because some of the transaction amount was spent on fees that have yet to mature and be seen as spendable.

The above shows that the client’s blockchains and mempools are in sync. If they are not, wait a few seconds and try the calls above again as it may take a moment for the nodes to synchronize. They display the same results because they are connected nodes on the same Liquid network and broadcast transactions and blocks between each other in very much the same was as Bitcoin nodes do.

Now let’s examine the transaction as it is seen by Bob’s wallet and also how it is seen from the point of view of Alice’s wallet. First the view from Bob’s wallet:

Bob:~$ elements-cli gettransaction $TXID

The output from that initially looks like just a huge random assortment of letters and numbers (the hex value of the transaction), but if you scroll up you will see some more readable content above that.

Looking in the “details” section near the top, you will see that there are two amount values:

"details": [
  {
    ...
    "category": "send",
    "amount": -1.00000000,
    ...
  },
  {
    ...
    "category": "receive",
    "amount": 1.00000000,
    ...
  }
]

And so we can confirm that Bob’s wallet can view the actual amounts being sent and received in this transaction. This is because the blinded transaction was sent from Bob’s own wallet and so it has access to the required data to unblind the amount values. You will also see two other properties and their values within the two details sections: “amountblinder” and “assetblinder”. These indicate that both the asset amount and the type of asset were blinded. This ensures that wallets without knowledge of the blinding key are prevented from viewing them.

Looking at the transaction from Alice’s wallet, we would expect both amount and type to be unknown as they were sent using a Confidential Transaction.

In order to check Alice’s view of the transaction, we need Alice’s node to use the value of the transaction id that we stored in the TXID variable in Bob’s terminal session. When our code examples use a variable that was set by the other node like this, we will assume that you will set the variable across terminal sessions. This can be done by using echo to print the value within one terminal session, copying the value, and then setting it within the other node’s terminal session, like so:

Bob:~$ echo $TXID

Copy the result, which will be similar to:

533533c5a382ccf14f4b432130f02871091b6a28594a9481da12f360f711685d

And then we can set a corresponding variable in Alice’s terminal session, similar to doing the following:

Alice:~$ TXID=533533c5a382ccf14f4b432130f02871091b6a28594a9481da12f360f711685d

Tip

You can use this technique whenever we need to use a variable set in one terminal session within another.

This then allows us to run the code below.

Alice:~$ elements-cli gettransaction $TXID

This causes an error. The reason is that Alice’s wallet will not contain wallet details of the transaction as it does not relate to an address contained in her wallet. We can get the raw transaction data from Alice’s node’s copy of the blockchain using the getrawtransaction command like this:

Alice:~$ elements-cli getrawtransaction $TXID 1

That returns raw transaction details. If you look within the “vout” section you can see that there are three instances. The first two instances are the receiving and change amounts and the third is the transaction fee. Of these three amounts, the fee is the only one in which you can see a value, as the fee itself is unblinded. For the first two instances you will see (amongst others) properties with values similar to this:

"value-minimum": 0.00000001,
"value-maximum": 11258999.06842624,
"amountcommitment": "0881c61d8a15ad26e6ef621ca99a188ccebbdb348d5285012393459b7e5b1e6113",
"assetcommitment": "0b1b7a1a4a604f4a68b3277e3a8926d74e86adce7b92e8e6ba67f9c5a8ad2cbcf4",

What this shows are the “blinded ranges” of the value amounts and the commitment data that acts as proof of the actual amount and type of asset transacted. The raw view of the transaction will be the same accross all nodes, regardless of if they hold the blinding key or not, only the results of gettransaction from a wallet aware of the blinding key used will show the actual amounts.

Even if we were to import Bob’s private key into Alice’s wallet it would still not be able to see the amounts and type of asset using gettransaction because it still has no knowledge of the blinding key used.

If we want to let Alice’s wallet view the actual amount details we’ll need to import the address as ‘watch only’ so gettransaction will work, and then import the blinding key so we can see the unblinded amounts. First, import and then view the transaction. Note the use of the second argument passed to gettransaction, set to true. This tells gettransaction to include watch only addresses, such as the one we imported. Remember to copy the value of $ADDR from Bob’s session and set it in Alice’s before running the code below.

Alice:~$ elements-cli importaddress $ADDR
Alice:~$ elements-cli gettransaction $TXID true

This time the call to gettransaction does not error but, because Alice still does not know the blinding key, the amount (towards the top of the output) will show as:

"amount": {
  "bitcoin": 0.00000000

Without knowledge of the Blinding Key, the amount and type of asset being transacted is still hidden.

In order for anyone else apart from the sender and receiver of a Confidential Transaction (such as an auditor) to view the amount and type of assets being transacted, they need to know the blinding key that was used to generate the blinded address. To show this, we can export the blinding key Bob’s wallet used for the related address, import it into Alice’s wallet and try to view the transaction again. Let’s export the key for that particular address from Bob’s wallet and import it into Alice’s.

Export Bob’s blinding key for the address:

Bob:~$ BOBBLINDINGKEY=$(elements-cli dumpblindingkey $ADDR)

Echo, copy and set the variable accross terminal sessions again like we did above (steps not shown) and then import Bob’s blinding key into Alice’s wallet:

Alice:~$ elements-cli importblindingkey $ADDR $BOBBLINDINGKEY

Now that Alice’s wallet has knowledge of the blinding key used on that address, we can run the checks we did above from Alice’s wallet, this time expecting to see the actual amount value:

Alice:~$ elements-cli gettransaction $TXID true

Magic! Alice’s wallet now shows the actual value sent in the transaction.

"amount": {
  "bitcoin": 1.00000000

We’ve seen that the use of a blinding key hides the amount and type of assets in an address and that by importing the right blinding key, we can reveal those values. In practical use, a blinding key may be given to an auditor, should there be a need to verify the amounts and types of assets held by a party. The Confidential Transactions feature of Liquid also allows for “range proofs” to be performed without the need to expose actual amounts. This allows statements such as “address abc holds at least an amount x of asset y” to be cryptographically proven as true or false.


Issued Assets

In this example we’ll issue our own asset, label it, look at the re-issuance token, and learn how to send the asset to other other Liquid nodes and wallet addresses. We’ll also take a look at how to keep track of what assets have been issued and re-issued.

First, let’s take a look at Alice’s wallet to see what it currently holds.

Alice:~$ elements-cli getwalletinfo

We see that Alice holds a lot of the “bitcoin” asset and nothing else:

"balance": {
  "bitcoin": 101.00000000
}

Every asset you issue within Liquid (including the “bitcoin” default) will be assigned its own hex value. This is used to uniquely identify it on the network. Notice how “bitcoin” is displayed with a readable asset name. This is because Liquid automatically associates the label “bitcoin” with the asset hex for that default asset. To find out its hex value we can run:

Alice:~$ elements-cli dumpassetlabels

Which returns:

"bitcoin": "cc5fb67403bee3b9a74e7518b7684d6cb64041e4156970a17aa653ee336b1097"

One of the main features of Liquid is the ability to issue your own assets.

Tip

There is nothing inherently different between assets in the way they are handled within the Liquid protocol.

Run the following to perform a blinded issuance of 100 of a new asset, along with 1 reissuance token for the asset.

Alice:~$ ISSUE=$(elements-cli issueasset 100 1)

Tip

To manually issue an asset using the rawissueasset command, please see the Creating Raw Issuances example. Additionally, the Proof of Issuance example shows how to prove that you were the one who issued the asset using the “contract hash” parameter.

That will create a new asset type, an initial supply of 100 and also 1 reissuance token. The reissuance token is used to prove authority to reissue more of the asset at a later date. We have issued one such token in the command above. The token is transferable and you can initially create as many as you think you will need based upon how many of the network participants will need to perform this duty. Each asset has its own reissuance token. We’ll look at this in more detail later. It is also possibly to create an asset with no reissuance tokens by using zero for that argument.

Tip

The minimum amount of an asset you can issue is 0.00000001. This is also the smallest amount of any issued asset that you can send. The minimum amount of the default asset that you can send (L-BTC) is higher, at 0.00001. These differ as the default asset considers the minimum send amount of the Bitcoin network’s dust limit. The maximum amount of an asset you can issue is 21,000,000 although more can be created by reissuing. When you create the reissuance token as above, where the amount was 1, you are actually creating 100,000,000 of them when measured in their smallest denomination. This is because they are divisible like every other asset on Liquid. The 1 is little more than a user interface concession. For ease of readability we will refer to this issuance as “one token”.

We have stored all of the returned data from the issuance command in a variable named “ISSUE”, which we’ll pull the hex of the new asset from, storing that value in another variable named “ASSET”. We’ll also store the “token” value (which we’ll explain and use later) and the “txid” and “vin” of the issuance, which will be used when we try and unblind the issuing transaction shortly.

In order to do this we can use a tool called jq (which we installed as part of the tutorial dependencies) to strip out and store only the parts returned and saved within “ISSUE” that we are interested in:

Alice:~$ ASSET=$(echo $ISSUE | jq '.asset' | tr -d '"')
Alice:~$ TOKEN=$(echo $ISSUE | jq '.token' | tr -d '"')
Alice:~$ ITXID=$(echo $ISSUE | jq '.txid' | tr -d '"')
Alice:~$ IVIN=$(echo $ISSUE | jq '.vin' | tr -d '"')

To see the hex identifier for the asset we issued run:

Alice:~$ echo $ASSET

Which will return something like this:

f0379482f9b77917670be0f060cc58debc6d93db0bf857458d5fb19080c8ab67

In order to view all asset issuances that have been made we run the “listissuances” command:

Alice:~$ elements-cli listissuances

That will show the asset we just issued. You’ll notice tha it has the following property:

"isreissuance": false,

This indicates that it was an original issuance and not a reissuances. You’ll also see that the newly issued asset does not have an “assetlabel”.

Tip

Asset labels are not part of network protocol consensus and are local only to each node. You should not rely on them for transaction processing but instead use the asset’s hex value, which is shared across the network.

You can set the label by assigning it against the hex identifier of the asset. This can be done in the relevant Liquid.conf file by adding a line:

assetdir=asset_hex_here:your_label_here

Or you can do this by passing in “assetdir” as a parameter when you start the node. We’ll do this now and call our new asset “demoasset”. We need to stop the node first.

Alice:~$ elements-cli stop
Alice:~$ elementsd -assetdir=$ASSET:demoasset
Alice:~$ elements-cli listissuances

This shows that the asset we issued has the label we assigned to its hex value:

"assetlabel": "demoasset",

Having labelled our asset for ease of reference, we will now look at the issuance data for “demoasset” in more detail. You will notice a “token” property similar to that below:

"token": "33244cc19dd9df0fd901e27246e3413c8f6a560451e2f3721fb6f636791087c7",

This is the hex of the token and it can be used to reissue the asset. Yours will likely differ from the actual value above. There is also a “tokenamount” property which corresponds to the amount we created:

"tokenamount": 1.00000000,

When the transaction has confirmed, we can take a look using Bob’s wallet to see if it is aware of Alice’s asset issuance:

Bob:~$ elements-cli listissuances

Bob’s wallet isn’t aware of the issuance, so we’ll import the address into his wallet.

In order to check Bob’s view of the issuance, we need Bob’s node to use the address used when Alice issued the asset. As that’s currently known to Alice’s node, we need to copy the information over to Bob’s node. This can be done by using echo to print the value within one terminal session, copying the value, and then setting it within the other node’s terminal session, like so:

Alice:~$ IADDR=$(elements-cli gettransaction $ITXID | jq '.details[0].address' | tr -d '"')
Alice:~$ echo $IADDR

Copy the result, which will be similar to:

AzpkDFjr6nNm4nncoBHuZr5S2zH4wPEpTvU63M8jNuU7JMXAVb5rVNCYmLB3fTmP9kLGgTAdCTwrrndu

And then we can set a corresponding variable in Bob’s terminal session, similar to doing the following:

Bob:~$ IADDR=AzpkDFjr6nNm4nncoBHuZr5S2zH4wPEpTvU63M8jNuU7JMXAVb5rVNCYmLB3fTmP9kLGgTAdCTwrrndu

Tip

You can use this technique whenever we need to use a variable set in one terminal session within another.

Bob can now import the address relating to Alice’s issuance as a watch-only address:

Bob:~$ elements-cli importaddress $IADDR

Another way to make Bob’s node aware of the issuance is for Bob to get the issuance transaction ID and use that to import any output address from the transaction into his wallet. This is useful if Bob is not able to get the adress from Alice, but knows the transaction in which the asset was issued… perhaps by using the Blockstream Explorer’s assets list to look it up. From that page Bob can either select one of the addresses from the outputs to import, as above, or use the TXID to derive an address.

Bob doesn’t need to run the following, as he’s already imported the address in the code above but, for reference, the code to use the transaction ID to derive an address to import is shown below. This method requires that index=1 is set in the config file of Bob’s node.

Bob:~$ ISSUE_RAW_TX=$(elements-cli getrawtransaction $ITXID 1)
Bob:~$ ISSUE_VOUTS=$(echo $ISSUE_RAW_TX | jq -r '.vout')
Bob:~$ VOUT_ADDRESS_ISSUE=$(echo $ISSUE_VOUTS | jq -r '.[0].scriptPubKey.addresses[0]')
Bob:~$ elements-cli importaddress $VOUT_ADDRESS_ISSUE

Either way, if we try and view the list of issuances from Bob’s node now we’ll see the issuance, but notice that the amount of the asset and the amount of its associated token are hidden:

Bob:~$ elements-cli listissuances

The asset amount and the token amount are both blinded and show as -1:

"tokenamount": -1,
"assetamount": -1,

In the Confidential Transactions example, we were able to expose the amount and type of asset being sent in a regular Confidential Transaction by exporting the blinding key used to create the blinded address and importing it into another wallet. We can do the same type of thing with the issuance transaction using the issuance blinding key.

First, we need to export the issuance blinding key. We refer to issuances by their txid/vin pair. As there is only one per input it will be zero, but we’ll use the value we saved earlier as it is good practice to not rely on such things staying fixed. You will need to echo, copy and create the appropriate variables (ISSUEKEY, ITXID, IVIN) in Bob’s node as we did above for this to work (steps not shown).

Alice:~$ ISSUEKEY=$(elements-cli dumpissuanceblindingkey $ITXID $IVIN)
Bob:~$ elements-cli importissuanceblindingkey $ITXID $IVIN $ISSUEKEY

Now when we run the command to list known issuances from Bob’s wallet we should see the actual values:

Bob:~$ elements-cli listissuances

Which returns:

"tokenamount": 1.00000000,
"assetamount": 100.00000000,

Indeed, Bob’s wallet can now see both the amount of the asset and its reissuance token that were issued.

Just like any other asset in Liquid, we can send our “demoasset” from Alice’s address to Bob’s using the “sendtoaddress” command. This differs from its implementation in Bitcoin’s source code in that it accepts an additional parameter, which allows you to specify the type of asset to be sent. Be aware that the step above where we imported the issuance blinding key is not required in order to transact an issued asset between addresses and wallets. Importing the issuance blinding key just enables another wallet to view the issuance details in full.

You will need to echo, copy and create the appropriate variable (BOBDEMOADD) in Alice’s node as we did above for this to work (steps not shown).

Bob:~$ BOBDEMOADD=$(elements-cli getnewaddress)
Alice:~$ elements-cli sendtoaddress $BOBDEMOADD 10 "" "" false false 1 UNSET demoasset

After confirmation, Bob’s wallet will now show an amount of 10 “demoasset” and Alice’s will show 90:

Bob:~$ elements-cli getwalletinfo
Alice:~$ elements-cli getwalletinfo

As we didn’t assign a label in Bob’s node for the asset we created, it will be identified by its hex value instead. We will therefore have to use the hex identifier instead of the asset label when we send it from his node. Remember that asset labels are local only to each node and are not part of the network’s protocol rules. We’ll demonstrate how Bob can send the asset using the hex value by transferring the 10 “demoasset” back to Alice:

You will need to echo, copy and create the appropriate variable (ALICEDEMOADD) in Bob’s node as we did above for this to work (steps not shown).

Alice:~$ ALICEDEMOADD=$(elements-cli getnewaddress)
Bob:~$ elements-cli sendtoaddress $ALICEDEMOADD 10 "" "" false false 1 UNSET $ASSET
Bob:~$ ADDRGEN=$(elements-cli getnewaddress)
Bob:~$ elements-cli generatetoaddress 1 $ADDRGEN

We should see that Bob’s wallet has no “demoasset” in it anymore and Alice’s is back to 100:

Bob:~$ elements-cli getwalletinfo
Alice:~$ elements-cli getwalletinfo

We can see that is indeed the case.


Reissuing Assets

In this section we’ll look at reissuing an amount of the asset we previously issued. We’ll re-use the asset hex we stored in the “ASSET” variable. Reissuing just means “creating some more of” in this context. For future use, we’ll store the result of the “reissueasset” command in a variable named “RTRANS” and from that, strip out the transaction ID, storing it in another variable named “RTXID”:

Alice:~$ RTRANS=$(elements-cli reissueasset $ASSET 99)
Alice:~$ RTXID=$(echo $RTRANS | jq '.txid' | tr -d '"')

We’ve just created 99 more units of our new asset. As an aside, because we already labelled the asset in the previous example, we could have also passed “demoasset” in as the first parameter instead of the hex identifier and it would have worked exactly the same.

To check this issuance history of our asset (and ignore the “bitcoin” issuance) we can run the “listissuances” command and specify the asset we are interested in:

Alice:~$ elements-cli listissuances $ASSET

For the above command you may also pass the asset label in instead of the hex value. As has been noted before, be aware that labels are stored locally and are not network-wide.

Along with the original issuance you should see a new entry with the following property:

"isreissuance": true,

This property allows us to differentiate between initial issuances and reissuances. Note that the transaction id where amounts of the asset were created is also included in the returned data.

Let’s look at the details of the transaction where we reissued our asset:

Alice:~$ elements-cli gettransaction $RTXID

Scroll to the top of the returned transaction data as there are a few things worth noting here. The first is that within the “amount” section we can see that 0 “bitcoin” and 99 “demoasset” were transacted:

"amount": {
  "bitcoin": 0.00000000,
  "33244cc19dd9df0fd901e27246e3413c8f6a560451e2f3721fb6f636791087c7": 0.00000000,
  "demoasset": 99.00000000,

This information suggests that an Liquid transaction can transact more than one type of asset within the same transaction, which is indeed the case.

Tip

To send different types of asset in the same transaction, the “sendmany” command is used. The syntax is the same as in Bitcoin.

The “amount” section shows the net effect of the transaction as: 0 “bitcoin”, 99 “demoasset” and also another asset that is 0. That unlabelled asset is our reissuance token (the hex for which will differ from that above but the results are otherwise the same). What this shows is that once the sent and received amounts are totalled we have created 99 “demoasset”. You can see how the values in the “amount” section are derived by scrolling down the returned data and looking within the “details” section.

You will see that amounts of 99 “demoasset” and 1 reissuance token were sent:

"category": "send",
"amount": -99.00000000,

"category": "send",
"amount": -1.00000000,

And that further on the same amounts were received:

"category": "receive",
"amount": 99.00000000,

"category": "receive",
"amount": 1.00000000,

We can see how the “amount” section above therefore lists the net transfer of 0 (-1 +1) tokens. The reason why the net of this is the creation of 99 new “demoasset” is that the “reissueasset” command essentially spends from a zero balance address, and so the received amount has the effect of creating 99 new “demoasset”. The 99 new “demoasset” are basically spent into existence. It is worth highlighting again that in order to reissue an asset you must hold a related reissuance token. They must therefore be allocated wisely.

To check that the blinding works the same for a reissuance transaction as it does for a normal transaction we can check Bob’s view of Alice’s reissuance transaction. Wait until a block is confirmed to let the nodes sync, then run the following:

Bob:~$ RAWRTRANS=$(elements-cli getrawtransaction $RTXID)
Bob:~$ elements-cli decoderawtransaction $RAWRTRANS

We can see that the amounts and asset types are indeed blinded with results like this:

"value-minimum": 0.00000001,
"value-maximum": 42.94967296,

You could unblind these using the techniques we used for the initial issuance should you want to.

We have seen that reissuance is just a special kind of spending transaction whereby you can create more of the original asset, so long as you hold a valid reissuance token in your wallet. Next we will look at how to transfer the reissuance tokens.

Let’s send the reissuance token from Alice to Bob so that Bob can reissue some “demoasset” himself. Note that if there was always going to be a need for them both to reissue the asset at the same time, we could have just created two reissuance tokens and initially sent one to Bob, leaving Alice still holding the other. Either way, we would need to send from one wallet to the other, so let’s begin. First we’ll double check that Alice’s wallet currently holds the reissuance token and Bob’s does not:

Alice:~$ elements-cli getwalletinfo
Bob:~$ elements-cli getwalletinfo

Alice’s wallet has “bitcoin”, “demoasset” and the demo asset’s reissuance token whereas Bob’s only has “bitcoin”.

We’ll just prove that the token is needed to reissue by trying to reissue from Bob’s wallet without the token:

Bob:~$ elements-cli reissueasset $ASSET 10

That fails as expected and gives the following error message:

No available reissuance tokens in wallet.

So let’s send the reissuance token to Bob so that he can reissue some of our “demoasset”. Have Bob’s wallet generate a new address and save it in a variable:

Bob:~$ RITRECADD=$(elements-cli getnewaddress)

Send the token from Alice’s wallet to Bob’s new address as if it were any other asset. We’ll use the hex of the token to say what type of asset we are sending and also generate a block so the transaction confirms:

Alice:~$ elements-cli sendtoaddress $RITRECADD 1 "" "" false false 1 UNSET $TOKEN
Alice:~$ elements-cli generatetoaddress 1 $ADDRGEN

Check that Bob’s wallet now has the token and that Alice’s no longer does:

Alice:~$ elements-cli getwalletinfo
Bob:~$ elements-cli getwalletinfo

The token and right to reissue it provides is now Bob’s!

Tip

Remember from an earlier note that we can divide a reissuance token like any other asset in Liquid. Our send of “1” token in this instance actually transferred 100,000,000 of the smallest possible amount of the token. You can try sending something like “0.1” of the token back to Alice and check if she is again able to reissue (she will and so will Bob, who will still hold “0.9”).

Bob still doesn’t have any of the “demoasset” itself yet but, now that his wallet holds the reissuance token, he can reissue any amount of “demoasset” and it will show in his wallet:

Bob:~$ RISSUE=$(elements-cli reissueasset $ASSET 10)
Bob:~$ elements-cli getwalletinfo

Bob’s wallet now has “bitcoin”, the reissuance token for our new asset, and an amount of the new asset itself:

"balance": {
  "bitcoin": 10499998.99841940,
  "78ee1e3b9f2edf730e7f624e9d0f92d3e1d364c0ee91525bbccf56377dcd5033": 1.00000000,
  "600010d2a60cf0d9395dced79af3ccdb7c908e80cddf125ed1af80dc87186aae": 10.00000000
},

Remember that the new asset we issued will still only display using its hex value in Bob’s wallet as we didn’t assign it a label like we did in Alice’s wallet. In order for Alice to see this reissuance we need to make her wallet aware of it. Show that Alice’s wallet can’t see it:

Alice:~$ elements-cli listissuances

Import the address so that it can:

Bob:~$ RITXID=$(echo $RISSUE | jq '.txid' | tr -d '"')
Bob:~$ RIADDR=$(elements-cli gettransaction $RITXID | jq '.details[0].address' | tr -d '"')
Alice:~$ elements-cli importaddress $RIADDR

Another way to make Alices’s node aware of the reissuance is for Alice to get the reissuance transaction ID and use that to import any output address from the transaction into her wallet. This is useful if Alice is not able to get the adress from Bob, but knows the transaction in which the asset was reissued… perhaps by using the Blockstream Explorer’s assets list to look it up. From that page Alice can either select one of the addresses from the outputs to import, as above, or use the TXID to derive an address.

Alice doesn’t need to run the following, as she’s already imported the address in the code above but, for reference, the code to use the transaction ID to derive an address to import is shown below. This method requires that index=1 is set in the config file of Alice’s node.

Alice:~$ RITXID=$(echo $RISSUE | jq -r '.txid')
Alice:~$ REISSUE_RAW_TX=$(elements-cli getrawtransaction $RITXID 1)
Alice:~$ REISSUE_VOUTS=$(echo $REISSUE_RAW_TX | jq -r '.vout')
Alice:~$ VOUT_ADDRESS_REISSUE=$(echo $REISSUE_VOUTS | jq -r '.[0].scriptPubKey.addresses[0]')
Alice:~$ elements-cli importaddress $VOUT_ADDRESS_REISSUE

Either way, Alice’s wallet can now see the reissuance:

Alice:~$ elements-cli listissuances

As expected though, the amounts are blinded. You can unblind by importing the blinding key as we did earlier should you want to.

In Liquid you can also carry out an unblinded asset issue:

Bob:~$ UBRISSUE=$(elements-cli issueasset 55 1 false)
Bob:~$ UBASSET=$(echo $UBRISSUE | jq '.asset' | tr -d '"')

Which shows up as normal in Bob’s wallet after he issues it:

Bob:~$ elements-cli getwalletinfo

And this time if we import the address into Alice’s wallet (using either the address provided by Bob or by deriving an address from the issuance’s transaction ID) she should be able to see the amount issued, proving it was issued unblinded. Following the same process as before to import a known issuance address into Alice’s wallet:

Alice:~$ elements-cli listissuances
Bob:~$ UBRITXID=$(echo $UBRISSUE | jq '.txid' | tr -d '"')
Bob:~$ UBRIADDR=$(elements-cli gettransaction $UBRITXID | jq '.details[0].address' | tr -d '"')
Alice:~$ elements-cli importaddress $UBRIADDR

We can now see that Alice’s wallet can see both the issuance and the amount issued (55) without the need to import the blinding key:

Alice:~$ elements-cli listissuances

It may also be necessary to destroy asset amounts as well as create them in an Liquid based blockchain. This is easily done using the “destroyamount” command:

Bob:~$ elements-cli destroyamount $UBASSET 5

Check the amount has gone from the 55 issued down to 50:

Bob:~$ elements-cli getwalletinfo

It will show the amount as 50, proving that an amount of 5 were indeed destroyed:

"balance": {
  "4021bf6faac59d7ec593859a741318752f72e637e7d5ecfa54725dba1508771b": 50.00000000,

Creating, reissuing and destroying assets is a key feature of Liquid that can help you reflect the real world movement of assets or tokens on another blockchains etc.


Creating Raw Transactions

In this example we will create a raw transaction sending some of an issued asset.

Issue an asset and get the asset hex so we can send some:

Alice:~$ ISSUE=$(elements-cli issueasset 100 1)
Alice:~$ ASSET=$(echo $ISSUE | jq '.asset' | tr -d '"')

Check the asset shows up in our wallet:

Alice:~$ elements-cli getwalletinfo

Get a list of unspent we can use as inputs, referenced via transaction id, vout and amount:

Alice:~$ UTXO=$(elements-cli listunspent 0 0 [] true '''{"''asset''":"'''$ASSET'''"}''')
Alice:~$ TXID=$(echo $UTXO | jq '.[0].txid' | tr -d '"' )
Alice:~$ VOUT=$(echo $UTXO | jq '.[0].vout' | tr -d '"' )
Alice:~$ AMOUNT=$(echo $UTXO | jq '.[0].amount' | tr -d '"' )

Get an address to send the asset to, we’ll use an unconfidential address:

Alice:~$ ADDR=$(elements-cli getnewaddress)
Alice:~$ VALIDATEADDR=$(elements-cli validateaddress $ADDR)
Alice:~$ UNCONADDR=$(echo $VALIDATEADDR | jq '.unconfidential' | tr -d '"')

Build the raw transaction, sending an amount of 3 of the asset:

Alice:~$ SENDAMOUNT="3.00"
Alice:~$ RAWTX=$(elements-cli createrawtransaction '''[{"''txid''":"'''$TXID'''", "''vout''":'$VOUT', "''asset''":"'''$ASSET'''"}]''' '''{"'''$UNCONADDR'''":'$SENDAMOUNT'}''' 0 false '''{"'''$UNCONADDR'''":"'''$ASSET'''"}''')

Fund the transaction:

Alice:~$ FRT=$(elements-cli fundrawtransaction $RAWTX)

Blind and sign the transaction:

Alice:~$ HEXFRT=$(echo $FRT | jq '.hex' | tr -d '"')
Alice:~$ BRT=$(elements-cli blindrawtransaction $HEXFRT)
Alice:~$ SRT=$(elements-cli signrawtransactionwithwallet $BRT)
Alice:~$ HEXSRT=$(echo $SRT | jq '.hex' | tr -d '"')

Send the raw transaction:

Alice:~$ TX=$(elements-cli sendrawtransaction $HEXSRT)

After the transaction has confirmed, decode the raw transaction so we can see the amount of the asset sent and the address it went to:

Alice:~$ GRT=$(elements-cli getrawtransaction $TX)
Alice:~$ DRT=$(elements-cli decoderawtransaction $GRT)

Creating Raw Issuances

Get an address to issue the asset to:

Alice:~$ ASSET_ADDR=$(elements-cli getnewaddress "" legacy)

Get an address to issue the reissuance token to:

Alice:~$ TOKEN_ADDR=$(elements-cli getnewaddress "" legacy)

Create the raw transaction and fund it:

Alice:~$ RAWTX=$(elements-cli createrawtransaction '''[]''' '''{"''data''":"''00''"}''')
Alice:~$ FRT=$(elements-cli fundrawtransaction $RAWTX)
Alice:~$ HEXFRT=$(echo $FRT | jq '.hex' | tr -d '"')

Create the raw issuance:

Alice:~$ ASSET_AMOUNT=100
Alice:~$ TOKEN_AMOUNT=1
Alice:~$ RIA=$(elements-cli rawissueasset $HEXFRT '''[{"''asset_amount''":'$ASSET_AMOUNT', "''asset_address''":"'''$ASSET_ADDR'''", "''token_amount''":'$TOKEN_AMOUNT', "''token_address''":"'''$TOKEN_ADDR'''", "''blind''":false}]''')

The results of which include the following for reference:

Alice:~$ HEXRIA=$(echo $RIA | jq '.[0].hex' | tr -d '"')
Alice:~$ ASSET=$(echo $RIA | jq '.[0].asset' | tr -d '"')
Alice:~$ ENTROPY=$(echo $RIA | jq '.[0].entropy' | tr -d '"')
Alice:~$ TOKEN=$(echo $RIA | jq '.[0].token' | tr -d '"')

Blind, sign and send the transaction that creates the asset issuance:

Alice:~$ BRT=$(elements-cli blindrawtransaction $HEXRIA true '''[]''' false)
Alice:~$ SRT=$(elements-cli signrawtransactionwithwallet $BRT)
Alice:~$ HEXSRT=$(echo $SRT | jq '.hex' | tr -d '"')
Alice:~$ ISSUETX=$(elements-cli sendrawtransaction $HEXSRT)

Confirm and check it worked:

Alice:~$ elements-cli generatetoaddress 101 $(elements-cli getnewaddress)
Alice:~$ elements-cli listissuances
Alice:~$ elements-cli getwalletinfo

Proof of Issuance - Blockstream’s Liquid Asset Registry

Simple Issuance Example

This example shows how to use Blockstream’s Liquid Asset Registry. The asset registry allows you to register an asset and prove ownership against a domain name.

The code below creates an asset, an associated token, and outputs three files:

  • A liquid-asset-proof-<asset-id> file that must be placed on the server of the registered domain over the entire lifecycle of the asset.

  • A register_asset_<asset-id>.sh file that, when run, will post the asset registration data to Blockstream’s Liquid Asset Registry.

  • A delete_asset_<asset-id>.sh file that, when run, will delete the asset from the registry.

The use of the output files will be explained later.

There are seven fields that need setting within the script and one (collection) that can optionally be provided:

  • NAME The name of the asset as it will appear in the registry and applications that use asset registry data, such as the Blockstream Explorer. Length must be 5 to 255 ASCII characters.

  • TICKER The ticker you would like to assign to the asset or, in the case of a unique asset such as an NFT, a serial number or human-readable unique string. Length must be 3 to 24 characters. Valid characters are: a-z A-Z 0-9 . and -. If provided, ticker has to be unique when combined with the domain field. If the ticker is empty there is no such constraint.

  • DOMAIN The domain that will be used to verify the asset. Must be a valid domain name format and only supports (sub)domain names, for example: example.com or sub.example.com. Do not include the http/s or www prefixes. To verify you control the entity domain name, you’ll need to place a file on your webserver. Note that serving the file with https is required, except for .onion hidden services. The file should be made available at https://<domain>/.well-known/liquid-asset-proof-<asset-id>, with the following contents (See line 133 of the code example below for an example of file content generation): Authorize linking the domain name <domain> to the Liquid asset <asset-id>

  • ASSET_AMOUNT The amount to be issued. It is preferable to consider one satoshi unit as representing one asset. So to issue an amount of 100 of an asset, a value of 0.00000100 should be used. Please note that how this value is displayed within applications that use asset registry data is affected by the PRECISION field.

  • TOKEN_AMOUNT The amount of reissuance tokens to be created. It is preferable to consider one satoshi unit as representing one reissuance token, so a value of 0.00000001 will create 0.00000001 reissuance tokens and when viewed from Liquid Core it will also show as 0.00000001. When viewed from an app that uses satoshi sized units, such as the Blockstream Explorer it will show as 1. This field is not affected by the PRECISION field.

  • PRECISION The precision used to display the asset amount within applications that use Liquid Asset Registry data, such as the Blockstream Explorer. Represents the number of digits after the decimal point, i.e. 0 for non-divisible assets or 8 for BTC-like divisible assets. See the examples below.

Precision examples:

A PRECISION value of 0 for an ASSET_AMOUNT of 0.00000100 will create 0.00000100 when viewed from Liquid Core and 100 when viewed in an app that uses the asset registry data, which shifts the decimal 0 places left from the sats value.

A PRECISION value of 2 for an ASSET_AMOUNT of 0.00000100 will create 0.00000100 when viewed from Liquid Core and 1.00 when viewed in an app that uses the asset registry data, which shifts the decimal 2 places left from the sats value.

A PRECISION value of 8 for an ASSET_AMOUNT of 0.00000100 will create 0.00000100 when viewed from Liquid Core and 0.00000100 when viewed in an app that uses the asset registry data, which shifts the decimal 8 places left from the sats value. This equates to BTC units.

The default is 0 and the maximum value is 8.

Please note that precision is not taken into account when displaying reissuance token amounts. So if you issue your asset with 1 reissuance token it will always show as 100 000 000 (the amount 1.00 in sats) via Blockstream Explorer and other apps using the registry. In Elements it will show as 1.00000000.

  • PUBKEY Holds the public key value that goes into the issuer_pubkey registration data field. The code example derives this from a new address generated by a private key in the node’s wallet.dat file. For safety, this file should be backed up after running the script. Any ECDSA secp256k1 public key could be used in its place, which may be of use when the issuing entity is not the same as the entity running the script. You should ensure that, whatever PUBKEY value you use to set the registration data’s issuer_pubkey field, it is recorded somewhere and that the corresponding private key is backed up for potential future use in authorizing amendments to the registry data. Note that the script sets the PUBKEY variable’s value by calling getaddressinfo with a new address generated by the node’s wallet (line 52). The value of PUBKEY is then used to set the issuer_pubkey registration data field (line 64). The issuer_pubkey data field is used by the Asset Registry to verify ownership when removing or updating registry data in the future.

  • COLLECTION An optional field to relate a group of uniquely issued assets, as is often helpful for a collection of NFTs. Length must be 1 to 255 ASCII characters. If sub-categorization is helpful, consider a convention such as a / delimiter, e.g. “Top-level-collection/Sub-collection”.

The code below uses Liquid in live mode. The code can easily be adapted for test use.

Save the code below in a file named issue_and_prepare_register.sh and take a back up of the script before you run it as you may wish to refer to it in the future to track how you created the registry data.

  1#!/bin/bash
  2set -x
  3
  4shopt -s expand_aliases
  5
  6# ASSUMES elementsd IS ALREADY RUNNING
  7
  8######################################################
  9#                                                    #
 10#    SCRIPT CONFIG - PLEASE REVIEW BEFORE RUNNING    #
 11#                                                    #
 12######################################################
 13
 14# Amend the following:
 15NAME="your asset name here"
 16TICKER="your ticker"
 17# Do not use a domain prefix in the following:
 18DOMAIN="domain.here"
 19# Issue 100 assets using the satoshi unit, dependant on PRECISION when viewed from
 20# applications using Asset Registry data.
 21ASSET_AMOUNT=0.00000100
 22# Issue 1 reissuance token using the satoshi unit, unaffected by PRECISION.
 23TOKEN_AMOUNT=0.00000001
 24
 25# Amend the following if needed:
 26PRECISION=0
 27
 28# Optional collection parameter. Set to "" to ignore:
 29COLLECTION="Your-top-level-collection/Your-sub-collection"
 30
 31# Asset registry url
 32# When using test-net replace to
 33# https://assets-testnet.blockstream.info/
 34ASSET_REGISTRY_URL="https://assets.blockstream.info/"
 35
 36# Don't change the following:
 37VERSION=0
 38
 39# Change the following to point to your elements-cli binary and liquid live data directory (default is .elements).
 40alias e1-cli="elements-cli -datadir=$HOME/.elements"
 41
 42# We will hash using sha256sum if available, openssl otherwise (other options are available)
 43which sha256sum >/dev/null 2>&1 && alias sha256hash="sha256sum | sed 's/ .*//g'" || alias sha256hash="openssl dgst -sha256 | sed 's/.*= //g'"
 44
 45##############################
 46#                            #
 47#    END OF SCRIPT CONFIG    #
 48#                            #
 49##############################
 50
 51# Exit on error
 52set -o errexit
 53
 54# We validate characters in the domain
 55echo $DOMAIN| grep -q '[^a-z0-9\.-]' && RV=$? || RV=$?
 56if [ $RV -eq 0 ];then
 57    echo "invalid chars detected in the domain, exiting...."
 58    exit -1
 59fi
 60
 61# We will be using the issueasset command and the contract_hash argument:
 62# issueasset <assetamount> <tokenamount> <blind> <contract_hash>
 63
 64# As we need to sign the deletion request message later we need
 65# a legacy address. If you prefer to generate a pubkey and sign
 66# outside of Elements you can use a regular address instead.
 67NEWADDR=$(e1-cli getnewaddress "" legacy)
 68
 69VALIDATEADDR=$(e1-cli getaddressinfo $NEWADDR)
 70
 71PUBKEY=$(echo $VALIDATEADDR | jq -r '.pubkey')
 72
 73ASSET_ADDR=$NEWADDR
 74
 75NEWADDR=$(e1-cli getnewaddress "" legacy)
 76
 77TOKEN_ADDR=$NEWADDR
 78
 79# Create the contract and calculate the contract hash
 80# The contract is formatted for use in the Blockstream Asset Registry:
 81if [ "$COLLECTION" = "" ]; then
 82    CONTRACT='{"entity":{"domain":"'$DOMAIN'"},"issuer_pubkey":"'$PUBKEY'","name":"'$NAME'","precision":'$PRECISION',"ticker":"'$TICKER'","version":'$VERSION'}'
 83else
 84    CONTRACT='{"collection":"'$COLLECTION'","entity":{"domain":"'$DOMAIN'"},"issuer_pubkey":"'$PUBKEY'","name":"'$NAME'","precision":'$PRECISION',"ticker":"'$TICKER'","version":'$VERSION'}'
 85fi
 86
 87CONTRACT_HASH=$(echo -n "${CONTRACT}" | sha256hash)
 88
 89# Reverse the hash --- expects an even length
 90TEMP=$CONTRACT_HASH
 91LEN=${#TEMP}
 92until [ $LEN -eq "0" ]; do
 93    END=${TEMP:(-2)}
 94    CONTRACT_HASH_REV="$CONTRACT_HASH_REV$END"
 95    TEMP=${TEMP::$((${#TEMP} - 2))}
 96    LEN=$((LEN-2))
 97done
 98
 99# Issue the asset and pass in the contract hash
100IA=$(e1-cli issueasset $ASSET_AMOUNT $TOKEN_AMOUNT false $CONTRACT_HASH_REV)
101
102# Details of the issuance...
103ASSET=$(echo $IA | jq -r '.asset')
104TOKEN=$(echo $IA | jq -r '.token')
105ISSUETX=$(echo $IA | jq -r '.txid')
106
107#####################################
108#                                   #
109#    ASSET REGISTRY FILE OUTPUTS    #
110#                                   #
111#####################################
112
113# Output the proof file - you need to place this on your domain.
114echo "Authorize linking the domain name $DOMAIN to the Liquid asset $ASSET" > liquid-asset-proof-$ASSET
115
116# Create the bash script to run after you have placed the proof file on your domain
117# that will call the registry and request the asset is registered.
118echo "curl $ASSET_REGISTRY_URL --data-raw '{\"asset_id\":\"$ASSET\",\"contract\":$CONTRACT}'" > register_asset_$ASSET.sh
119
120# Create the bash script to delete the asset from the registry (if needed later)
121PRIV=$(e1-cli dumpprivkey $ASSET_ADDR)
122SIGNED=$(e1-cli signmessagewithprivkey $PRIV "remove $ASSET from registry")
123echo "curl -X DELETE $ASSET_REGISTRY_URL$ASSET -H 'Content-Type: application/json' -d '{\"signature\":\"$SIGNED\"}'" > delete_asset_$ASSET.sh
124
125# Stop the daemon
126e1-cli stop
127sleep 10
128
129echo "Completed without error"

When you have saved the above to the file, edit the variables at the top and of the file and start elements-qt or elementsd using an argument of -server=1 to allow the Liquid client to communicate with it. Execute the script from the directory you created it in by opening a Terminal session and running:

bash issue_and_prepare_register.sh

In order to register the asset just created:

  • Wait a couple of minutes for the issuance transaction to confirm.

  • Place the liquid-asset-proof-<asset-id> file in a folder named .well-known in the root of your domain. This proof should be stored there over the entire lifecycle of the asset.

  • Run the register_asset_<asset-id>.sh script.

For example, if your domain was your-example-domain-here.com and the asset id generated was 123abc (it will of course be much longer) then the file generated would be named:

liquid-asset-proof-123abc

The domain variable in the code above would be set to:

your-example-domain-here.com

So the path used to check asset to domain registry would end up being:

your-example-domain-here.com/.well-known/liquid-asset-proof-123abc

Once that file is accessible you can then run the register_asset_<asset-id>.sh script and, when the required checks against the domain and issuance transaction have been made, the registration will be found on Blockstream’s Liquid Asset Registry.

You can remove the asset from the registry by running delete_asset_<asset-id>.sh.

Asset Registry Specifications

Contract JSON Fields

Required fields:

  • version: currently 0

  • issuer_pubkey: the hex-encoded public key of the issuer

  • name: 1-255 ASCII characters

  • entity: the online entity linked to this asset which currently only supports (sub)domain names in the form of a nested object with {"domain":"foobar.com"}

Optional fields:

  • ticker: 3-24 characters consisting of a-z, A-Z, 0-9, . and -. If provided, has to be unique within the entity (domain name) namespace.

  • precision: number of digits after the decimal point, i.e. 0 for non-divisible assets or 8 for BTC-like. defaults to 0.

  • collection: 1-255 ASCII characters.

Example:

{
  "version": 0,
  "issuer_pubkey": "037c7db0528e8b7b58e698ac104764f6852d74b5a7335bffcdad0ce799dd7742ec",
  "name": "Foo Coin",
  "ticker": "FOO",
  "entity": { "domain": "foo-coin.com" },
  "precision":8
}

Contract Hash

The contract hash is the (single) sha256 hash of the contract json document, canonicalized to have its keys sorted lexographically.

The canonicalization can be done with Perl like so:

$ perl -e 'use JSON::PP; my $js = JSON::PP->new; $js->canonical(1); print $js->encode($js->decode($ARGV[0]))' '{"version":0,"name":"FOO",...}'

Or with Python like so:

$ python -c 'import json,sys; sys.stdout.write(json.dumps(json.loads(sys.argv[1]), sort_keys=True, separators=(",",":")))' '{"version":0,"name":"FOO",...}'

Or with JavaScript using the json-stable-stringify library.

The resulting sha256 hash needs to be reversed to match the format expected by elementsd, similarly to the reverse encoding of txids and blockhashes as originally implemented for bitcoin by Satoshi. This can be done in a unix environment like so:

echo <hash> | fold -w2 | tac | tr -d "\n"

All together:

$ CONTRACT='{"version":0,"ticker":"FOO","name":"Foo Coin"}'
$ CONTRACT_HASH=$(python -c 'import json,sys; sys.stdout.write(json.dumps(json.loads(sys.argv[1]), sort_keys=True, separators=(",",":")))' "$CONTRACT" | sha256sum | head -c64 | fold -w2 | tac | tr -d "\n")
$ echo $CONTRACT_HASH

Domain Ownership Proof

To verify you control the entity domain name, you’ll need to make a file on your webserver available at https://<domain>/.well-known/liquid-asset-proof-<asset-id>, with the following contents:

Authorize linking the domain name <domain> to the Liquid asset <asset-id>

Note that serving the file with https is required, except for .onion hidden services.

Issuing & Registering Assets

Prepare your contract json and get your contract hash. You can verify their validity before issuing the asset using the validation endpoint:

$ curl https://assets.blockstream.info/contract/validate -H 'Content-Type: application/json' \
      -d '{"contract": <your-contract-json>, "contract_hash": "<your-contract-hash>"}'

If everything seems good, issue the asset using elementsd’s rawissueasset with your hash as the contract_hash parameter and take note of the resulting asset_id. Add the domain ownership proof (as described above) and, once the issuance transaction confirms, submit the asset to the registry:

$ curl https://assets.blockstream.info/ -H 'Content-Type: application/json' \
      -d '{"asset_id": "<asset-id>", "contract": <contract-json>}'

Deleting Asset Registry Data

Deleting an asset from the Blockstream Asset Registry only removes its metadata from the registry. It does not, and can not, delete the asset itself from the Liquid Blockchain.

To delete an asset from the Blockstream Asset Registry, you need to sign and send a message authorizing the deletion. The message must be signed using the private key for the address associated with the issuer_pubkey used in registration.

You can use Elements to sign the message. First, dump the private key for the address that was used to get the pubkey:

elements-cli dumpprivkey <address>

Use the returned private key to sign a message of the format remove <asset-id> from registry.

The call to elements to sign the message is:

elements-cli signmessagewithprivkey <private key> "remove <asset-id> from registry"

The result of this is a base64 encoded signature. Post the signed message/signature to the asset registry like so:

$  curl -X DELETE https://assets.blockstream.info/<asset-id> -H 'Content-Type: application/json' \
    -d '{"signature":"<base64-encoded-signature>"}'

You will receive an Asset deleted message in response.

The asset will immediately be removed from the Liquid Asset Registry data. It may take a short while before the data deletion is replicated in the Blockstream Explorer itself.


Liquid and Bitcoin Transaction Differences

Here’s a short technical explainer that highlights the differences in the data of a Bitcoin and a Liquid Network transaction. Transaction data in Liquid and Bitcoin is similar in many ways because Liquid is built on top of Bitcoin’s codebase. But Liquid transactions contain some additional fields and the data is returned in slightly different formats.

Note

For a line-by-line explanation of everything returned in a Liquid transaction open Elements Core, go to Window > Console and enter each of the commands into the text field.

Contents

Calling gettransaction

When calling gettransaction in Liquid, the amount is returned as an array, which shows the amount of each asset held.

"amount": {
    "bitcoin": 0.00000000
  },

When calling gettransaction in Bitcoin, an array is not returned, only as a simple property and value pair.

<<code block>>
 "amount": 0.00000000,

In Liquid, the fee amount is returned as an array, which shows the amount of the asset used to pay fees.

"fee": {
    "bitcoin": -0.00043100
  },

In Bitcoin, the fee is not returned as an array, only as a simple property and value pair.

"fee": -0.00003740,

Liquid introduces three new fields that are returned in gettransaction that show the asset being transacted along with the amount being transacted and asset blinders (when applicable).

Bitcoin and Liquid:

"confirmations": 101,
  "blockhash": "7ebd6c337a0f414f22d0246aa7ec7feb3742030b2ab9fdb660120b717dedaf94",
  "blockindex": 1,
  "blocktime": 1554811774,
  "txid": "5fa4ef7898eb704dbaea16634ab68302826c42198c9a5f9ff3e60622f83aac90",
  "walletconflicts": [
  ],
  "time": 1554811735,
  "timereceived": 1554811735,
  "bip125-replaceable": "no",
  "details": [
    {
    "address": "2NGSJetH6NMAMimt3MBUWe4Eb9zetevw2Tt",
    "category": "send",
    "amount": -1.00000000,

Liquid:

"amountblinder": "07284ca3100c24a2e3bc0483ade3a462468074b7b207375ccfb0f27846c99ea2",
    "asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
    "assetblinder": "846663fc5318aa9ea602449c8d598c9cd43459c0056971eff44fba464d89b328",

Bitcoin and Liquid:

"label": "",
    "vout": 0,
    "fee": 0.00043100,
    "abandoned": false
    },
    {
    "address": "2NGSJetH6NMAMimt3MBUWe4Eb9zetevw2Tt",
    "category": "receive",
    "amount": 1.00000000,

Liquid shows the asset being transacted along with the amount and asset blinders (when applicable).

Liquid:

"amountblinder": "07284ca3100c24a2e3bc0483ade3a462468074b7b207375ccfb0f27846c99ea2",
"asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
"assetblinder": "846663fc5318aa9ea602449c8d598c9cd43459c0056971eff44fba464d89b328",

Bitcoin and Liquid:

"label": "",
    "vout": 0
  }
],
"hex": "0200000001…

Calling decoderawtransaction

When calling decoderawtransaction in Liquid or Bitcoin, the data returns something similar to shown below, with the differences between the data returned in Liquid highlighted.

Bitcoin and Liquid:

{
  "txid": "5fa4ef7898eb704dbaea16634ab68302826c42198c9a5f9ff3e60622f83aac90",
  "hash": "6d5f1b8965db82ef37a6ec7381de4a09f0369a3ec3ac236d17384738974232b2",
  "wtxid": "6d5f1b8965db82ef37a6ec7381de4a09f0369a3ec3ac236d17384738974232b2",
  "withash": "95151cf350baf2c99c0ff36e1f4110989b1f33e0d32d8c302c95ad5ecd79e8a2",
  "version": 2,
  "size": 7525,
  "vsize": 2155,
  "weight": 8620,
  "locktime": 303,
  "vin": [
    {
      "txid": "956e34fb9ace5709183e8bed4555c5d701781735633cfa17e9c1c68fb2462ee1",
      "vout": 0,
      "scriptSig": {
      "asm": "00144e4f612e206ec2a27cd0818630bb3304526cc56a",
      "hex": "1600144e4f612e206ec2a27cd0818630bb3304526cc56a"
    },

Liquid:

The is_pegin field denotes if the transaction was a peg-in transaction.

"is_pegin": false,

Bitcoin and Liquid:

"sequence": 4294967293,
  "txinwitness": [
    "304402201da54cfd...<continues>",
    "039c1a8daee141ef32399e3dab9e04bd9c4239342577e94e9e58b644c8dc58ba98"]
  }],
"vout": [
  {

Liquid:

The additional fields listed below are linked to the use of Confidential Transactions (CTs) in Liquid.

"value-minimum": 0.00000001,
"value-maximum": 687.19476736,
"ct-exponent": 0,
"ct-bits": 36,
"valuecommitment": "0844778d24db8b3454924e3b77d2aa00b4bd57bc20cb852a65238a336b93db7ac6",
"assetcommitment": "0b96a62e05fcf65a50ad58643a603f21bb033172336c653840accbae54e9fe7dd7",
"commitmentnonce": "02acc6606bdd8c65bdaeadf1eefec726d0d3b777586922b6255c557bb8e43ac946",
"commitmentnonce_fully_valid": true,

Bitcoin:

But Bitcoin only uses the value field.

"value": 48.99996260,

Bitcoin and Liquid:

  "n": 0,
  "scriptPubKey": {
  "asm": "OP_HASH160 fe635cfa8bb86040c3871d7e973e7e155583f7f9 OP_EQUAL",
  "hex": "a914fe635cfa8bb86040c3871d7e973e7e155583f7f987",
  "reqSigs": 1,
  "type": "scripthash",
  "addresses": [
    "2NGSJetH6NMAMimt3MBUWe4Eb9zetevw2Tt"
  ]
  }
},

Liquid:

In Liquid, the fee is explicitly stated as a vout. It is not derived from deducting the vout total from the vin total like in Bitcoin.

{
  "value": 0.00043100,
  "asset": "b2e15d0d7a0c94e4e2ce0fe6e8691b9e451377f6e46e8045a86f7c4b5d4f0f23",
  "commitmentnonce": "",
  "commitmentnonce_fully_valid": false,
  "n": 2,
  "scriptPubKey": {
  "asm": "",
  "hex": "",
  "type": "fee"
  }
}

Peg-out data

In a peg-out transaction, Liquid introduces an additional set of fields that are included in the vout section.

"pegout_chain": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
"pegout_asm": "OP_DUP OP_HASH160 bd3432857da35ae7c93a05c911208aad0d6fa695 OP_EQUALVERIFY OP_CHECKSIG",
"pegout_hex": "76a914bd3432857da35ae7c93a05c911208aad0d6fa69588ac",
"pegout_reqSigs": 1,
"pegout_type": "pubkeyhash",
"pegout_addresses": [
  "1JFREzVxcivkeRQpngZ9ERYaoFuSP9o3vG"
]