> For the complete documentation index, see [llms.txt](https://docs.wizz.cash/wizz-wallet/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.wizz.cash/wizz-wallet/zh/kai-fa-zhe/electrumx-zhi-nan/bu-fen-ran-se-zi-ding-yi-ran-se.md).

# 部分染色 / 自定义染色

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

使用 v1.5.\* 的 Tag 可以运行相关的索引代码。有关接口结构变动的具体说明可以查看： [API 集成](/wizz-wallet/zh/kai-fa-zhe/electrumx-zhi-nan/api-ji-cheng.md)

### 部分染色

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

<figure><img src="/files/BwrNXjeMAHTt1mUVdrr7" alt=""><figcaption></figcaption></figure>

#### 全染色 -> 部分染色

<table><thead><tr><th>输入/输出</th><th width="249">Satoshis</th><th>Atomicals</th></tr></thead><tbody><tr><td>vin:0</td><td>1000</td><td>1000 ATOM</td></tr><tr><td>vin:1</td><td>9900</td><td>0 ATOM</td></tr><tr><td>vout:0</td><td>900</td><td>900 ATOM</td></tr><tr><td>vout:1</td><td>10000</td><td>100 ATOM</td></tr></tbody></table>

如果没有部分染色，当交易尝试分配 900 ATOM 到 `vout:0` 时，剩下的 *100 ATOM* 会被燃烧。现在部分染色会尝试将这 *100 ATOM* 分配给 `vout:1`，由于 `sat_value >= atomical_value`，分配可以进行。

| 输入/输出  | Satoshis | Atomicals |
| ------ | -------- | --------- |
| 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` 。

#### 部分染色 -> 全染色

| 输入/输出  | Satoshis | Atomicals |
| ------ | -------- | --------- |
| vin:0  | 1000     | 100 ATOM  |
| vin:1  | 1000     | 900 ATOM  |
| vout:0 | 1000     | 1000 ATOM |
| vout:1 | 1000     | 0 ATOM    |

对两个部分染色的 UTXO 进行合并会自动分配完成。

***

### 自定义染色

自定义染色通过 `z` 操作符将 Atomicals 分配到 *546* 以下。分配的资产没有小数，且仍然需要符合基本转账规则。

<figure><img src="/files/r1ekcuSlzZOUd6r7MpuW" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
在 Atomical 数量可以小于 *546* 的情况下 UTXO 的含聪量仍然需要大于 *546* sats，最重要的规则是 含聪量 >= Atomical 数量。含聪量和 Atomical 数量可以在同样规则下任意组合。
{% endhint %}

#### 操作注意事项

`z` 操作符相对全能，但最好只在特定场景下使用。以下是一些关于 `z` 的注意事项：

1. 最好仅在所有输出的 Atomicals 都小于 *546* 时使用，否则相对普通转账而言会造成少量的体积增加。
2. 染色资产必须小于等于 UTXO 的 sats，否则**多出的分配资产会被燃烧**。

#### 主要步骤

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

1. OP 设置为十六进制的 `z` (`7a`)；&#x20;
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` 操作中完成这两个操作的实际行为。开发者需要根据使用场景来选择使用的操作符。

#### 自定义染色代码示例

```typescript
// 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);
```

{% hint style="warning" %}
当前代码示例仅支持P2TR地址类型
{% endhint %}

查看[完整代码示例](https://github.com/WizzWallet/wizzwallet-provider-demo/blob/main/src/Split.tsx)

#### 如何对Reveal PSBT进行签名

```typescript
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。
