部分染色 / 自定义染色

描述 Atomicals 部分染色和自定义染色的实现细节及如何集成。

Atomicals ElectrumX v1.5.0 版本新增了部分染色和自定义染色的功能,这些功能在正式网区块高度 848484 开启(测试网 2584936)。

使用 v1.5.* 的 Tag 可以运行相关的索引代码。有关接口结构变动的具体说明可以查看: API 集成

部分染色

部分染色可以让 Atomicals 在部分不燃烧的场景下分配到小于 546。用户无需直接感知部分染色,其仅作为 Atomicals 转账规则的一个补充子集。以下是一些 Atomicals 不再会被直接燃烧的场景:

全染色 -> 部分染色

输入/输出SatoshisAtomicals

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,分配可以进行。

输入/输出SatoshisAtomicals

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

部分染色 -> 全染色

输入/输出SatoshisAtomicals

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 的注意事项:

  1. 最好仅在所有输出的 Atomicals 都小于 546 时使用,否则相对普通转账而言会造成少量的体积增加。

  2. 染色资产必须小于等于 UTXO 的 sats,否则多出的分配资产会被燃烧

主要步骤

在进行自定义染色时,你应该遵循以下规则:

  1. OP 设置为十六进制的 z (7a);

  2. 将染色的记录放在 payload,结构为 (TypeScript):Record<string, Record<number, number>>

  3. 针对染色记录构造对应的 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