部分染色 / 自定义染色
描述 Atomicals 部分染色和自定义染色的实现细节及如何集成。
Atomicals ElectrumX v1.5.0 版本新增了部分染色和自定义染色的功能,这些功能在正式网区块高度 848484 开启(测试网 2584936)。
使用 v1.5.* 的 Tag 可以运行相关的索引代码。有关接口结构变动的具体说明可以查看: API 集成
部分染色
部分染色可以让 Atomicals 在部分不燃烧的场景下分配到小于 546。用户无需直接感知部分染色,其仅作为 Atomicals 转账规则的一个补充子集。以下是一些 Atomicals 不再会被直接燃烧的场景:

全染色 -> 部分染色
vin:0
1000
1000 ATOM
vin:1
9900
0 ATOM
vout:0
900
900 ATOM
vout:1
10000
100 ATOM
如果没有部分染色,当交易尝试分配 900 ATOM 到 vout:0
时,剩下的 100 ATOM 会被燃烧。现在部分染色会尝试将这 100 ATOM 分配给 vout:1
,由于 sat_value >= atomical_value
,分配可以进行。
vin:0
1000
1000 ATOM
vin:1
546
0 ATOM
vout:0
999
999 ATOM
vout:1
547
1 ATOM
另一个例子是分配 vin:0
中的 1 ATOM(或者 999 ATOM)到 vout:1
。此处的 vout:1
只要大于被分配的 Atomicals 资产值即可,即可以为 sats_values>=546
。
部分染色 -> 全染色
vin:0
1000
100 ATOM
vin:1
1000
900 ATOM
vout:0
1000
1000 ATOM
vout:1
1000
0 ATOM
对两个部分染色的 UTXO 进行合并会自动分配完成。
自定义染色
自定义染色通过 z
操作符将 Atomicals 分配到 546 以下。分配的资产没有小数,且仍然需要符合基本转账规则。

在 Atomical 数量可以小于 546 的情况下 UTXO 的含聪量仍然需要大于 546 sats,最重要的规则是 含聪量 >= Atomical 数量。含聪量和 Atomical 数量可以在同样规则下任意组合。
操作注意事项
z
操作符相对全能,但最好只在特定场景下使用。以下是一些关于 z
的注意事项:
最好仅在所有输出的 Atomicals 都小于 546 时使用,否则相对普通转账而言会造成少量的体积增加。
染色资产必须小于等于 UTXO 的 sats,否则多出的分配资产会被燃烧。
主要步骤
在进行自定义染色时,你应该遵循以下规则:
OP 设置为十六进制的
z
(7a
);将染色的记录放在
payload
,结构为 (TypeScript):Record<string, Record<number, number>>
。针对染色记录构造对应的 UTXO,每条 UTXO 要符合
sat_value >= atomical_value
。
以下是一个 payload
示例:
{
"9527efa43262636d8f5917fc763fbdd09333e4b387afd6d4ed7a905a127b27b4i0": {
0: 1000,
1: 2000
},
"8888e25790cc118773c8590522e1cab2ccc073a9375b238aaf9aadb13a764a13i0": {
2: 3000,
3: 4000
}
}
上面的 payload
表示了将 ID 为 9527...
的 ARC-20 分配 1000 到 vout:0
,分配 2000 到 vout:1
;将 ID 为 8888...
的 ARC-20 分配 3000 到 vout:2
,分配 4000 到 vout:3
。
拆分已合并的资产
由于 z
可以定义 Atomicals 的分配情况,它相当于是高级的 x
(splat) 和 y
(split) 操作的组合,可以在单个 z
操作中完成这两个操作的实际行为。开发者需要根据使用场景来选择使用的操作符。
自定义染色代码示例
// current connected account address info
const addressInfo = {
output: ...,
scripthash: ...,
network: ...,
}
// query all atomicals balance
const balance = await fetch(`${electrumUrl}/blockchain.atomicals.listscripthash?params=["${addressInfo.scripthash}",true]`, { signal: controller.signal }).then((res) => {
return res.json();
});
// pure utxos without any assets
const pureUTXOs = balance.utxos.filter((utxo: any) => {
if (Array.isArray(utxo.atomicals)) {
return utxo.atomicals.length == 0;
}
if (typeof utxo.atomicals === 'object') {
return Object.keys(utxo.atomicals).length === 0;
}
// unreachable
return false;
}).sort((a: any, b: any) => b.value - a.value);
const atomicals = balance.atomicals;
const utxos = balance.utxos;
const fs: Record<string, any> = {};
for (const utxo of utxos) {
// compatible with old format
if (Array.isArray(utxo.atomicals)) {
// ignore merged assets
if (utxo.atomicals.length !== 1) {
continue;
}
const atomicalId = utxo.atomicals[0];
const atomical = 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 format
else if (typeof utxo.atomicals === 'object') {
// ignore merged assets
if (Object.keys(utxo.atomicals).length !== 1) {
continue;
}
for (const atomicalId in utxo.atomicals) {
const atomical = 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;
}
}
}
const allFTs = Object.values(fs);
// pick a FT from allFTs
const ftUTXOs = selectedFT.utxos;
// send ft amount
const amount = 1;
// input ft value
let inputFTValue = 0;
// remainder ft value
let remainderFTValue = 0;
// input ft utxos
const revealInputs = [];
for (const utxo of ftUTXOs) {
inputFTValue += utxo.atomical_value;
revealInputs.push(utxo);
remainderFTValue = inputFTValue - amount;
if (remainderFTValue >= 0) {
break;
}
}
const payload: Record<string, Record<number, number>> = {};
const revealOutputs = [
{
// 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 config
const buffer = new AtomicalsPayload(payload).cbor();
// user's public key to xpub
const selfXOnly = toXOnly(Buffer.from(publicKey, 'hex'));
// use `z` op type
const { scriptP2TR, hashLockP2TR } = prepareCommitRevealConfig('z', selfXOnly, buffer, addressInfo.network);
const hashLockP2TROutputLen = hashLockP2TR.redeem!.output!.length;
// calculate fee
const revealFee = calculateAmountRequiredForReveal(feeRate, revealInputs.length, revealOutputs.length, hashLockP2TROutputLen);
// calculate need for reveal transaction
const revealNeed = revealFee + revealOutputs.reduce((acc, output) => acc + output.value, 0) - revealInputs.reduce((acc, input) => acc + input.value, 0);
// prepare commit transaction
// reveal transaction output
const outputs = [
{
address: scriptP2TR.address!,
value: revealNeed,
},
];
const inputs = [];
let inputSats = 0;
let ok = false;
let fee = 0;
// calculate utxo inputs and fee
for (const utxo of 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) {
throw new Error('Insufficient funds');
}
// create commit psbt
const commitPsbt = new bitcoin.Psbt({ network: addressInfo.network });
for (const input of 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 hash
const tx = commitPsbt.__CACHE.__TX as Transaction;
const txId = tx.getId();
// create reveal psbt
const revealPsbt = new bitcoin.Psbt({ network: addressInfo.network });
// build tap leaf script
const tapLeafScript = {
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 as any],
});
// add reveal inputs
for (const revealInput of 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 psbt
const signedCommitPsbt = await window.wizz.signPsbt(commitPsbt.toHex());
// sign reveal psbt with `signAtomical` option
const signedRevealPsbt = await window.wizz.signPsbt(revealPsbt.toHex(), { signAtomical: true });
// broadcast commit transaction
const commitTxId = await window.wizz.pushPsbt(signedCommitPsbt);
// broadcast reveal transaction
const revealTxId = await window.wizz.pushPsbt(signedRevealPsbt);
当前代码示例仅支持P2TR地址类型
查看完整代码示例
如何对Reveal PSBT进行签名
function signRevealPsbt(keyFor: ECPairInterface, psbtHex: string, network: Network) {
const psbt = Psbt.fromHex(psbtHex, { network });
const childNodeXOnlyPubkey = toXOnly(keyFor.publicKey);
const tapLeafScript = psbt.data.inputs[0].tapLeafScript![0] as TapLeafScript;
const customFinalizer = (_inputIndex: number, input: any) => {
const witness = [input.tapScriptSig[0].signature, tapLeafScript.script, tapLeafScript.controlBlock];
return {
finalScriptWitness: witnessStackToScriptWitness(witness),
};
};
psbt.signInput(0, keyFor);
psbt.finalizeInput(0, customFinalizer);
const tweakedChildNode = keyFor.tweak(bitcoin.crypto.taggedHash('TapTweak', childNodeXOnlyPubkey));
for (let i = 1; i < psbt.data.inputs.length; i++) {
psbt.signInput(i, tweakedChildNode);
psbt.finalizeInput(i);
}
return psbt.toHex();
}
解析交易
索引在 v1.5.0 版本中提供了接口查询 PSBT 或者原始 Tx 的方法。/proxy/blockchain.atomicals.decode_psbt
可以用来解析 PSBT,/proxy/blockchain.atomicals.decode_tx
可以用来解析原始 Tx。
Last updated