Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guides/go/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ The wallet-cli versions of the same workflows live in the [command-line guides](
| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs |
| [Wallet & signing (WASM)](wallet-wasm.md) | Generate wallets, derive addresses, sign without a daemon |
| [Forging custom transactions](custom-transactions.md) | Mixed inputs/outputs, protocol fees, multi-party signing, intents |
| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output |
2 changes: 1 addition & 1 deletion docs/guides/go/staking.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,4 @@ To estimate rewards: pool block stats for a time range, the pool's `cost_per_blo

## Manual transaction building

The `wasm` package encodes delegation transactions without the wallet daemon: `EncodeOutputCreateDelegation`, `EncodeOutputDelegateStaking`, `EncodeInputForWithdrawFromDelegation` (nonce from the indexer's `NextNonce`). See [Go SDK: Staking](../../build/sdks/go/staking.md) for the full low-level flow.
The `wasm` package encodes delegation transactions without the wallet daemon: `EncodeOutputCreateDelegation`, `EncodeOutputDelegateStaking`, `EncodeInputForWithdrawFromDelegation` (nonce from the indexer's `NextNonce`). See [Go SDK: Staking](../../build/sdks/go/staking.md) for the full low-level flow, and [Composing UTXOs](utxo-composition.md) for the spend-and-recreate composition pattern.
2 changes: 2 additions & 0 deletions docs/guides/go/trading-with-orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,5 @@ freezeInput, err := c.EncodeInputForFreezeOrder(orderID, currentBlockHeight, min
```

Concluding returns the unclaimed `give` remainder plus any accumulated `ask` balance to the order's conclude destination. Only the maker can freeze or conclude.

To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md).
171 changes: 171 additions & 0 deletions docs/guides/go/utxo-composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: "Composing UTXOs: Updating an Order"
description: "How to compose UTXOs on Mintlayer: update a DEX order atomically by consuming its conclude-order input and re-creating it with a create-order output."
sidebar_position: 9
---

# Composing UTXOs: Updating an Order

Mintlayer is a UTXO chain: **there is no mutable state**. A DEX order is an unspent output, and every protocol object behaves the same way, so "updating" an order means spending it and re-creating it *in the same transaction*. This atomic spend-and-recreate pattern is the essence of UTXO composition.

## The pattern

An order lives as a `CreateOrder` output holding the `give` balance plus any accumulated `ask` balance. Its spend authority is the maker, gated by the order's **nonce**. To update the order (new price, new amounts), build one transaction that:

1. consumes the order via a **conclude-order input** (this releases the whole order balance), and
2. re-locks the funds into a fresh **create-order output** with the new parameters.

```mermaid
flowchart LR
OLD["Order UTXO<br/>(give 100, ask 0)"] -->|"conclude-order input<br/>(nonce n)"| TX["Transaction"]
FEE["Coin UTXO<br/>(fee payer)"] --> TX
TX -->|"create-order output<br/>(give 100, ask 20)"| NEW["New order UTXO<br/>(new order id)"]
TX -->|"change"| MAKER["Maker address"]
```

Two consequences worth internalizing:

- The **new order gets a new ID** (IDs are derived from inputs, see [predicting IDs](custom-transactions.md#predicting-ids)). Publish the new ID and retire the old one.
- If the transaction fails or is dropped, nothing changed: the old order is still live and its nonce unspent.

:::note[What consensus allows and forbids]

The pattern above is exactly what consensus permits: **one order operation input** (fill, conclude, or freeze) — a slot shared with token account commands, so you cannot combine an order operation with a mint, an unmint, or any other command input — and **one `CreateOrder` output** per transaction. Concluding and re-creating in the same transaction sits precisely at both limits, which is what makes the atomic update work.

Everything past those limits is rejected by the verifier:

- A second command input (order operation or token account command) — `MultipleAccountCommands`.
- A second `CreateOrder` output — `MultipleOrdersCreated`.
- Spending a `CreateOrder` output as a UTXO input — orders are not UTXOs; they are consumed only through order operation inputs (`InvalidInputTypeInTx`).
- Creating an order in a transaction with no UTXO input — the new order's id is derived from the first UTXO outpoint (`NoUtxoInputsForOrderIdCreation`), which is why the fee input below is not optional.

:::

## The ingredients

| Piece | Encoder | Source of truth |
| ----- | ------- | --------------- |
| Conclude input | `EncodeInputForConcludeOrder(orderID, nonce, currentBlockHeight, network)` | `Nonce` from the indexer's order record |
| New order output | `EncodeCreateOrderOutput(askAmount, askTokenID, giveAmount, giveTokenID, concludeAddress, network)` | Your new price/amounts |
| Surplus / change outputs | `EncodeOutputTransfer` / `EncodeOutputTokenTransfer` | Old balance minus new give amount |
| Fee input | `EncodeInputForUtxo` | A coin UTXO you own |
| Witness for the conclude input | `EncodeWitness` with `TxAdditionalInfo` | Order balances from the indexer |
| Witness for the coin input | `EncodeWitness` | n/a |

## Fetch the order state

```go
import "github.com/mintlayer/go-sdk/indexer"

idxClient := indexer.New("http://127.0.0.1:3000")

order, err := idxClient.GetOrder(ctx, oldOrderID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("give: %s ask: %s nonce: %d\n",
order.GiveBalance.Atoms(), order.AskBalance.Atoms(), order.Nonce)
```

The `AskBalance` matters: any asks accumulated from takers are part of the order you are concluding, so the new order (or a change output) must account for them.

## Build the transaction

Example: top up the `give` side and raise the ask. Old order: give 100 tokens / ask 10 coins. New order: give 150 / ask 12. The conclude releases 100 tokens + accumulated asks; a separate token UTXO adds the extra 50.

```go
// 1. Consume the old order
concludeInput, err := c.EncodeInputForConcludeOrder(
oldOrderID, order.Nonce, tip.BlockHeight, mintlayer.Mainnet)

// 2. Fee payer
srcID, err := c.EncodeOutpointSourceId(feeTxIDBytes, mintlayer.SourceTransaction)
feeInput, err := c.EncodeInputForUtxo(srcID, 0)

// 3. Re-create the order with updated parameters
var giveToken, askToken *string
newOrderOutput, err := c.EncodeCreateOrderOutput(
mintlayer.NewAmount("12"), askToken, // ask 12 coins (nil token id = coin)
mintlayer.NewAmount("150"), giveToken, // give 150 tokens
concludeAddress, // same conclude destination
mintlayer.Mainnet,
)

// 4. Any balance not re-locked returns via change outputs
changeOutput, err := c.EncodeOutputTransfer(
mintlayer.NewAmount("2000000000"), makerAddr, mintlayer.Mainnet) // fee change

tx, err := c.EncodeTransaction(
append(concludeInput, feeInput...),
append(newOrderOutput, changeOutput...),
0,
)
```

Token inputs (e.g. a UTXO holding the additional 50 tokens) encode with `EncodeInputForUtxo` like any other spend; only the *outputs* distinguish coin from token value.

## Sign it

The conclude input's sighash requires the order's balances via `TxAdditionalInfo`: this is the part that differs from a plain transfer. Signing is also where the inputs' backing UTXOs come in: `EncodeWitness` takes one entry per input, `0x00` for non-UTXO inputs (the conclude input) and `0x01` plus the encoded output for UTXO inputs (the fee input):

```go
feeOutput, err := c.EncodeOutputTransfer(
feeAmount, makerAddr, mintlayer.Mainnet, // re-encode the fee UTXO's Transfer output
)
allUtxoBytes := append([]byte{0x00, 0x01}, feeOutput...)

additionalInfo := mintlayer.TxAdditionalInfo{
OrderInfo: map[string]mintlayer.OrderInfo{
oldOrderID: {
InitiallyAsked: order.InitiallyAsked,
InitiallyGiven: order.InitiallyGiven,
AskBalance: order.AskBalance,
GiveBalance: order.GiveBalance,
},
},
}

var witnessBytes []byte
for i := 0; i < 2; i++ { // one witness per input, in input order
w, err := c.EncodeWitness(
mintlayer.SigHashAll,
makerKey,
makerAddr,
tx,
allUtxoBytes,
uint32(i),
additionalInfo, // required for the order input
tip.BlockHeight,
mintlayer.Mainnet,
)
if err != nil {
log.Fatal(err)
}
witnessBytes = append(witnessBytes, w...)
}

signedTx, err := c.EncodeSignedTransaction(tx, witnessBytes)
```

Submit via the indexer or node as usual, then predict and store the new order ID:

```go
newOrderID, err := c.GetOrderId(append(concludeInput, feeInput...), mintlayer.Mainnet)
```

## Variations of the same pattern

The conclude-then-recreate transaction is one instance of a general tool. With the same encoders you can:

- **Cancel an order**: conclude input, no create-order output; the balance flows to change outputs.
- **Self-fill and requote**: conclude input + `FillOrder`-style split: partially fill your own order and re-create a smaller one. (A pure fill by a taker uses `EncodeInputForFillOrder` and needs **no signature** for that input, `EncodeWitnessNoSignature`.)
- **Freeze before restructuring**: `EncodeInputForFreezeOrder` stops takers while you prepare the replacement transaction.
- **Chain protocol objects**: the same spend-and-recreate logic composes delegations (withdraw input + `EncodeOutputDelegateStaking` output) and any future output type.

:::note

Exactly which balances must be re-assigned to which outputs is enforced by consensus, not by the encoders, the runtime tells you *if* a transaction is invalid, the indexer tells you *what* the balances are, and the composition is yours to design.

:::

For the order model itself (give/ask, conclude key, freeze semantics), see [Trading with Orders](trading-with-orders.md) and the wallet-cli guide [Trading with orders](../cli/trading-with-orders.md).
2 changes: 1 addition & 1 deletion docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ New languages can be added alongside these; the workflows are the same, only the
| Manage a multisig address | [guide](cli/managing-a-multisig-address.md) | CLI only | CLI only | CLI only | CLI only |
| Migrate ERC-20 ML to mainnet | [guide](cli/migrate-erc20-token-to-mainnet.md) | Portal only | Portal only | Portal only | Portal only |

Deeper full-custody dives with the embedded WASM runtime (Go and Python, no wallet daemon): [Wallet & signing](go/wallet-wasm.md) and [Forging custom transactions](go/custom-transactions.md), each also available in the [Python guides](python/index.md). The Rust equivalent is the native `crypto` module, covered by [Cryptography](../build/sdks/rust/crypto.md) and [Building transactions](../build/sdks/rust/transactions.md).
Deeper full-custody dives with the embedded WASM runtime (Go and Python, no wallet daemon): [Wallet & signing](go/wallet-wasm.md), [Forging custom transactions](go/custom-transactions.md), and [Composing UTXOs](go/utxo-composition.md), each also available in the [Python guides](python/index.md). Composing UTXOs also exists in [JavaScript](javascript/utxo-composition.md) (raw `wasm-lib` encoders) and [Rust](rust/utxo-composition.md) (native `crypto` module, covered by [Cryptography](../build/sdks/rust/crypto.md) and [Building transactions](../build/sdks/rust/transactions.md)).
1 change: 1 addition & 0 deletions docs/guides/javascript/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ The wallet-cli versions of the same workflows live in the [command-line guides](
| [Issue an NFT](issue-nft.md) | MLS-03 issuance and transfers |
| [Staking and delegation](staking.md) | Create a delegation, stake, withdraw |
| [Trading with orders](trading-with-orders.md) | On-chain orders: create, discover, fill, conclude |
| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output |
| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs |
2 changes: 2 additions & 0 deletions docs/guides/javascript/trading-with-orders.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ The order creator can close an order and reclaim the remaining balance:
const signedTx = await client.concludeOrder('ord1...');
```

To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md).

## Example: market-maker loop

```ts
Expand Down
Loading