# 部分染色 / 自定义染色

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

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

### 部分染色

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

<figure><img src="https://2889738779-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FldTGMAu85aOIfm3hoGzh%2Fuploads%2FA7LKgWkZyq0SEdhUKcrD%2Fimage.png?alt=media&#x26;token=f56da544-e579-464e-b495-d96a66c2e53f" 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="https://2889738779-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FldTGMAu85aOIfm3hoGzh%2Fuploads%2FMR3jdbnA9YQWPOmKiqKn%2Fimage.png?alt=media&#x26;token=573d7a33-176d-480c-baa4-1a054ca3ff4d" 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。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.wizz.cash/wizz-wallet/zh/kai-fa-zhe/electrumx-zhi-nan/bu-fen-ran-se-zi-ding-yi-ran-se.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
