Describe the implementation detail of Particial Coloring / Custom Coloring of Atomicals and how to integrate with these features.
New coloring features are introduced with Atomicals ElectrumX v.1.5.0 and could be activated at the block height 848484 on the livenet (2584936 on the testnet).
The runnable code should be based on the v1.5.* tags. See the API Integration Guide for the migration guide for the breaking change.
Partial Coloring
The partial coloring ability allows Atomicals value to be divided less than 546 without burning in some cases. The user has no direct interaction with the feature because it is a subset of the Atomicals Transfer Rules. Here are some transaction cases that would not burn the Atomicals:
Fully colored -> Partially colored
When the transaction is trying to assign 900 ATOM to the vout:0, 100 ATOM is left which results in burning without the feature. Now, with the partial coloring, the transaction should try to assign the 100 ATOM to the vout:1, and it can since sats_value >= atomical_value.
Here is another example of the coloring. If you want to split 1 ATOM (or 999 ATOM) from the vin:0, assign the 1 ATOM to vout:1. The vout:1 could be 546 sats too since its sats_value >= atomical_value.
Partially colored -> Fully colored
With 2 partially colored UTXOs, assigning them to one specific output will be done automatically.
Custom Coloring
The custom coloring ability allows Atomicals value to be separated less than 546 by introducing the z operation. The value is without decimals and should follow the normal transfer rules.
Even if the Atomical value can be less than 546, the UTXO value should remain not less than 546. The important rule is sats_value >= atomical_value. Sats value and Atomical value can combined in any case based on the rule.
Notice of the operation
z is powerful, but it should be only used in certain cases to avoid more gas during transactions. Here are some notices about the z.
Use it only when the Atomicals value in all outputs is less than 546, or it could lead to extra size compared to a normal transfer transaction (cleanly assigned).
The coloring value must not be greater than the satoshis value of the UTXO, coloring values greater than the satoshis value will be reset to the satoshis value, and the remaining values will be burned.
Main Steps
To send a custom coloring transaction, you shall follow these instructions:
Put z as the OP code in HEX (7a).
Put the desired colored Atomicals value records into the payload. The record should follow the structure (in TypeScript): Record<string, Record<number, number>>.
Gather UTXOs to back coloring values, and make sure each UTXO has sat_value >= atomical_value.
The payload indicates that the ARC-20 with ID 9527... should have 1000 in the vout:0, and 2000 in the vout:1. And the ARC-20 with ID 8888... should have 3000 in the vout:2, and 4000 in the vout:3.
Split merged assets
Since the operation can define where Atomicals go, the z operator can be considered as the advanced version of x (splat) + y (split), their operations can be done during custom coloring in one single z transaction. Developers should choose between these operations according to use cases.
Custom coloring code example
// current connected account address infoconstaddressInfo= { output:..., scripthash:..., network:...,}// query all atomicals balanceconst balance = await fetch(`${electrumUrl}/blockchain.atomicals.listscripthash?params=["${addressInfo.scripthash}",true]`, { signal: controller.signal }).then((res) => {
returnres.json();});// pure utxos without any assetsconstpureUTXOs=balance.utxos.filter((utxo:any) => {if (Array.isArray(utxo.atomicals)) {returnutxo.atomicals.length==0; }if (typeofutxo.atomicals ==='object') {returnObject.keys(utxo.atomicals).length===0; }// unreachablereturnfalse;}).sort((a:any, b:any) =>b.value -a.value);constatomicals=balance.atomicals;constutxos=balance.utxos;constfs:Record<string,any> = {};for (constutxoof utxos) {// compatible with old formatif (Array.isArray(utxo.atomicals)) {// ignore merged assetsif (utxo.atomicals.length!==1) {continue; }constatomicalId=utxo.atomicals[0];constatomical= atomicals[atomicalId].data;if (atomical.type !=='FT') {continue; }utxo.atomical_value =utxo.value;if (atomical.utxos) {atomical.utxos.push(utxo);atomical.atomical_value +=utxo.value; } else {atomical.utxos = [utxo];atomical.atomical_value =utxo.value; } fs[atomicalId] = atomical; }// new formatelseif (typeofutxo.atomicals ==='object') {// ignore merged assetsif (Object.keys(utxo.atomicals).length!==1) {continue; }for (constatomicalIdinutxo.atomicals) {constatomical= atomicals[atomicalId].data;if (atomical.type !=='FT') {continue; }utxo.atomical_value =utxo.atomicals[atomicalId];if (atomical.utxos) {atomical.utxos.push(utxo);atomical.atomical_value +=utxo.atomical_value; } else {atomical.utxos = [utxo];atomical.atomical_value =utxo.atomical_value; } fs[atomicalId] = atomical; } }}constallFTs=Object.values(fs);// pick a FT from allFTsconstftUTXOs=selectedFT.utxos;// send ft amountconstamount=1;// input ft valuelet inputFTValue =0;// remainder ft valuelet remainderFTValue =0;// input ft utxosconstrevealInputs= [];for (constutxoof ftUTXOs) { inputFTValue +=utxo.atomical_value;revealInputs.push(utxo); remainderFTValue = inputFTValue - amount;if (remainderFTValue >=0) {break; }}constpayload:Record<string,Record<number,number>> = {};constrevealOutputs= [ {// send to any address address: toAddress,// ft value less than the dust amount(546) will be partially colored. value:Math.max(amount,546), },];payload[selectedFT.atomical_id] = {0: amount,};if (remainderFTValue) {revealOutputs.push({ address: address,// ft value less than the dust amount(546) will be partially colored. value:Math.max(remainderFTValue,546), }); payload[selectedFT.atomical_id][1] = remainderFTValue;}// prepare commit reveal configconstbuffer=newAtomicalsPayload(payload).cbor();// user's public key to xpubconstselfXOnly=toXOnly(Buffer.from(publicKey,'hex'));// use `z` op typeconst { scriptP2TR,hashLockP2TR } =prepareCommitRevealConfig('z', selfXOnly, buffer,addressInfo.network);consthashLockP2TROutputLen=hashLockP2TR.redeem!.output!.length;// calculate feeconst revealFee = calculateAmountRequiredForReveal(feeRate, revealInputs.length, revealOutputs.length, hashLockP2TROutputLen);
// calculate need for reveal transactionconst revealNeed = revealFee + revealOutputs.reduce((acc, output) => acc + output.value, 0) - revealInputs.reduce((acc, input) => acc + input.value, 0);
// prepare commit transaction// reveal transaction outputconstoutputs= [ { address:scriptP2TR.address!, value: revealNeed, },];constinputs= [];let inputSats =0;let ok =false;let fee =0;// calculate utxo inputs and feefor (constutxoof pureUTXOs) { inputSats +=utxo.value;inputs.push(utxo); fee =calculateFeesRequiredForCommit(feeRate,inputs.length,1);let v = inputSats - fee - revealNeed;if (v >=0) {if (v >=546) { fee =calculateFeesRequiredForCommit(feeRate,inputs.length,2); v = inputSats - fee - revealNeed;if (v >=546) {outputs.push({ address, value: v, }); } } ok =true;break; }}if (!ok) {thrownewError('Insufficient funds');}// create commit psbtconstcommitPsbt=newbitcoin.Psbt({ network:addressInfo.network });for (constinputof inputs) {commitPsbt.addInput({ hash:input.txid, index:input.index, sequence:0xfffffffd, witnessUtxo: { script:addressInfo.output, value:input.value, }, tapInternalKey: selfXOnly, });}commitPsbt.addOutputs(outputs);// get the transaction txid for reveal input utxo hashconsttx=commitPsbt.__CACHE.__TX asTransaction;consttxId=tx.getId();// create reveal psbtconstrevealPsbt=newbitcoin.Psbt({ network:addressInfo.network });// build tap leaf scriptconsttapLeafScript= { leafVersion: hashLockP2TR!.redeem!.redeemVersion, script: hashLockP2TR!.redeem!.output, controlBlock:hashLockP2TR.witness![hashLockP2TR.witness!.length-1],};revealPsbt.addInput({ sequence:0xfffffffd,// commit transaction txid hash: txId, index:0, witnessUtxo: { value: revealNeed, script:hashLockP2TR.output! }, tapLeafScript: [tapLeafScript asany],});// add reveal inputsfor (constrevealInputof revealInputs) {revealPsbt.addInput({ sequence:0xfffffffd, hash:revealInput.txid, index:revealInput.index, witnessUtxo: { value:revealInput.value, script:addressInfo.output }, tapInternalKey: selfXOnly, });}revealPsbt.addOutputs(revealOutputs);// sign commit psbtconstsignedCommitPsbt=awaitwindow.wizz.signPsbt(commitPsbt.toHex());// sign reveal psbt with `signAtomical` optionconstsignedRevealPsbt=awaitwindow.wizz.signPsbt(revealPsbt.toHex(), { signAtomical:true });// broadcast commit transactionconstcommitTxId=awaitwindow.wizz.pushPsbt(signedCommitPsbt);// broadcast reveal transactionconstrevealTxId=awaitwindow.wizz.pushPsbt(signedRevealPsbt);
To know the exact coloring result of a PSBT or a raw transaction, use /proxy/blockchain.atomicals.decode_psbt to decode a PSBT or /proxy/blockchain.atomicals.decode_tx to decode a raw transaction HEX string. These methods were added since v1.5.0 patch 2.