simplex_example
v0.1.0
Published
WebAssembly bindings for the SMPLX basic OLIV example builders.
Readme
Basic Simplex Example
This is an example project to get started with Simplex. The project assumes that the framework has already been successfully installed via simplexup.
Overview
The repository structure is the following:
root
├── simf — Simplicity smart contracts source directory
├── src — The project's Rust code (witness and transaction builders)
│ └── artifacts — Simplicity artifacts generated by Simplex (gitignored)
├── tests — Simplex tests directory
├── Cargo.toml — The project's Rust configuration
└── Simplex.toml — The project's Simplex configurationBuild
In order to see Simplex in action, first run the build command to generate Simplicity artifacts:
simplex buildYou will see that Simplex has generated a new artifacts directory in the src directory. Think of these artifacts as "rust typization" helpers for individual simplicity smart contracts in the project.
Please note that this new module gets declared in the lib.rs file.
Test
Once the artifacts are generated, let's run Simplex test "basic_test" inside basic_test.rs:
simplex test basic_test --nocaptureYou will see the test passing.
Under the hood, Simplex spins up a local Electrs + Elements regtest, establishes the connection, prefunds the signer specified in the simplex.toml, and runs the test marked via macros #[simplex::test].
Check out confidential_test.rs to learn how to issue and spend confidential UTXOs!
Oracle Asset Release / Reissuance Covenant
oracle_asset_release.simf is a small oracle-authorized mint covenant. It is not a
release of an already-issued asset. The covenant holds the reissuance token, and a
valid mint transaction spends that covenant UTXO and performs a Liquid reissuance.
The oracle payload is:
sha256(
domain |
asset_id |
asset_amount |
recipient_script_hash |
eth_txid |
ticket_txid |
ticket_vout
)The contract checks:
- at least 3 of the configured 5 oracle signatures verify against that payload
- the covenant/reissuance-token input is input 0
- the signed ticket UTXO is input 1
- the transaction has exactly 2 inputs
- input 1 prevout equals
ticket_txid:ticket_vout - the input 0 reissuance mints the signed
asset_id - the input 0 reissuance amount equals signed
asset_amount - output 0 sends signed
asset_idand signedasset_amountto signedrecipient_script_hash - output 1 returns the reissuance token to the same covenant script
Local Regtest Flow
Run the working end-to-end test:
simplex test oracle_asset_release_test --nocaptureThe test does two transactions:
Initial issuance transaction:
- issues
asset_idamount1 - issues reissuance token amount
1 - sends the issued asset to the signer wallet
- sends the reissuance token to the covenant script as a confidential output
- creates a small Liquid policy-asset ticket UTXO for fees/replay binding
- issues
Mint/reissuance transaction:
- spends the covenant-held reissuance token as input 0
- spends the signed ticket UTXO as input 1
- reissues the oracle-signed
asset_amount - sends the minted asset to the signed recipient
- returns the reissuance token to the covenant
External Oracle Testnet Address
The external oracle pubkeys are defined in src/oracle_asset_release.rs.
Generate the Liquid testnet covenant address using the hardcoded protocol covenant SLIP77 master blinding key:
cargo run --bin oracle_asset_release_addressThe hardcoded protocol covenant SLIP77 master blinding key is:
7b6d26da3eaa45718eb5e4af4c7cc29b8f9d78ddf43c59b3355472ce8fd24d95For the current oracle pubkeys and contract script, the generated testnet deposit address is:
tlq1pqfpqlc2xzs09tfpwxx4mvd3a63kcc5hxyzyjf9qvuzlxx5euj6a9vxe34zwjzfrush26qj82w5kumszfx8wuutq4jq2n49kjdd7ucn8axyxfz0h0px7jThe derived covenant blinding pubkey is:
02420fe146141e55a42e31abb6363dd46d8c52e6208924940ce0be63533c96ba56The address generator still accepts explicit overrides for comparison or experiments:
cargo run --bin oracle_asset_release_address -- \
--slip77-master-blinding-key <other_slip77_master_blinding_key_hex>cargo run --bin oracle_asset_release_address -- \
--blinding-pubkey <compressed_blinding_pubkey_hex>You can also run the Simplex smoke test:
simplex test --target oracle_asset_release_external_oracles_test \
external_oracle_testnet_deposit_address_is_derivable \
--nocaptureBlinding Key Requirement
Keep the reissuance-token covenant UTXO confidential for this SDK path.
Liquid reissuance needs issuance_blinding_nonce. The current Simplex SDK
derives that value from the confidential input secrets:
partial_input.secrets.asset_bfAn explicit reissuance-token UTXO has no secrets, so
FinalTransaction::add_program_issuance_input(...) cannot populate the nonce and
will fail with:
Reissuance input must be confidentialThat is transaction-building data required for this SDK reissuance path. It is not a covenant security check and should not be removed.
The protocol covenant SLIP77 master blinding key must be available to whoever
builds the mint transaction, because that builder must derive the covenant
script's blinding private key, unblind the covenant-held reissuance-token UTXO,
and create the reissuance input. For a ct(slip77(...),...) wallet, the
slip77(...) value is the master blinding key, not a compressed public key.
The #blinded=... fragment in a Blockstream Explorer URL is not the deposit
address blinding public key. It is output unblinding data for explorer display,
grouped as:
amount,asset_id,amount_blinder,asset_blinderThose values can prove or reveal an output's amount and asset to a third party. They are not enough to create the confidential covenant address, and they are not the builder's blinding private key.
Which Keys Are Used
There are three separate key roles:
- Issuer key: funds/signs the initial issuance transaction. This key is not needed by the covenant and is not needed by oracles. After the reissuance token is locked in the covenant, mint authority is controlled by the covenant plus oracle signatures.
- Protocol covenant SLIP77 master blinding key: derives the covenant script's blinding key. The mint transaction builder needs this private key material to unblind the covenant-held reissuance-token UTXO and derive the reissuance nonce required by the SDK.
- Oracle keys: sign only the oracle payload. They do not sign the Liquid transaction and they do not need the issuer key or blinding key.
NPM WebAssembly Package
The browser bindings for the OLIV transaction builders are published from this
example as the npm package simplex_example. Consumers should depend on the npm
package instead of a local ../smplx/examples/basic/pkg path.
From the repository root, build the package locally:
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
brew install llvm # macOS only, if clang cannot target wasm32
CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang ./scripts/build-basic-wasm-packageOn Linux CI the workflow installs clang, lld, and wasm-pack, then runs the
same script with CC_wasm32_unknown_unknown=clang.
Inspect the package before publishing:
npm pack --dry-run ./examples/basic/pkgPublish manually after logging in to npm:
npm publish ./examples/basic/pkg --access public --provenanceGitHub Actions publishes from .github/workflows/npm-basic-wasm.yml on version
tags, or from workflow_dispatch when publish is set to true. Configure npm
trusted publishing for the package, or add an npm automation token as the
repository secret NPM_TOKEN.
Testnet End-to-End Flow
Generate the covenant deposit address.
cargo run --bin oracle_asset_release_addressCapture:
protocol_covenant_slip77_master_blinding_key deposit_address blinding_pubkey script_pubkey script_hashCreate the initial issuance transaction.
This transaction creates the asset and the reissuance token. The reissuance token must be sent directly to the covenant address in this same transaction. After this confirms, the issuer no longer holds the reissuance token; the covenant output does.
Match the regtest shape:
output 0: issued asset, amount 1, issuer-controlled address output 1: Liquid policy asset ticket UTXO, builder-controlled address output 2: reissuance token, amount 1, confidential covenant deposit_addressIf your wallet reorders outputs, use the actual vouts from the confirmed transaction. The covenant token outpoint is the output that holds
reissuance_token_idamount1. The ticket outpoint is the Liquid policy asset output that will be spent as input 1 in the mint transaction.Capture from the issuance builder or wallet:
issuance_txid asset_id reissuance_token_id asset_entropy covenant_token_outpoint = issuance_txid:<vout holding reissuance_token_id> ticket_outpoint = issuance_txid:<vout holding the Liquid policy asset ticket>In the regtest example, these are printed by
oracle_asset_release_test.rsasasset_id,reissuance_token_id,asset_entropy, andissuance txid.Select the ticket UTXO.
Use the ticket UTXO created in step 2. It is included in the mint transaction as input 1. It pays fees and prevents oracle-signature replay.
Capture:
ticket_txid ticket_voutBuild the oracle payload.
Inputs:
domain = 0 asset_id asset_amount recipient_script_hash = sha256(recipient_script_pubkey) eth_txid ticket_txid ticket_voutPayload:
sha256(domain | asset_id | asset_amount | recipient_script_hash | eth_txid | ticket_txid | ticket_vout)Collect oracle signatures.
Any 3 of the 5 configured oracle private keys sign the payload hash. The oracles do not sign the Liquid transaction. They only sign the payload.
Build the mint/reissuance transaction.
The builder uses the hardcoded
protocol_covenant_slip77_master_blinding_keyto derive the covenant script's blinding private key and unblind the covenant token output before building this transaction.Transaction shape:
input 0: covenant_token_outpoint spends the confidential UTXO holding reissuance_token_id amount 1 carries the Liquid reissuance fields for asset_entropy and asset_amount input 1: ticket_txid:ticket_vout spends the signed ticket UTXO output 0: asset_id, asset_amount, recipient_script_pubkey output 1: reissuance_token_id, 1, covenant scriptIn the Rust SDK this is built with
FinalTransaction::add_program_issuance_input(...)andIssuanceInput::new_reissuance(asset_amount, asset_entropy). That call does not add another transaction input. It populates the Liquid reissuance fields on input 0, which is also the Simplicity covenant spend.The witness contains:
maybe_sigs domain asset_id asset_amount recipient_script_hash eth_txid ticket_txid ticket_voutBroadcast the mint transaction.
Expected result after confirmation:
output 0 gives recipient_script_pubkey asset_amount of asset_id output 1 returns reissuance_token_id amount 1 to the covenant script input 1 spends ticket_txid:ticket_voutThe same oracle signatures cannot be reused with a different ticket, because the signed payload commits to
ticket_txid:ticket_voutand the contract requires that exact outpoint at input 1. After the ticket is spent once, that signed mint payload is no longer usable.
Regtest
If you wish to keep the blockchain's state between tests, run a local regtest separately:
simplex regtestThis command sets up the standalone nodes that can be connected to via simplex configuration.
Update the simplex.toml file with the necessary URLs and credentials, then run the test again. You will see that the state gets preserved.
Disclaimer
GLHF!
