Skip to content

Reference

Lookup material, organised for finding one fact rather than for reading through.

To run it yourself, start with Run an allowance covenant end to end. For why any of this is shaped the way it is, see EXPLANATION.md.

Facts about Humid were established by reading its source; paths are relative to a Humid checkout. They were current when this was written and the project is moving.

Errors and what causes them

Message Cause Fix
{"reject": "unimplemented-construct"} on any withdrawal the relative-timelock bug apply the patch from Step 3c, rebuild, reload the extension
"FullWithdraw" pays nothing, so there is nothing to build. the change-only-output bug same patch
Unlock the wallet to continue. the wallet is locked open it from the Extensions button and enter the password; it re-locks periodically
X is neither a parameter nor an argument of this action usually instance single-nested instead of double-nested see the nesting rule below
[object Object] instead of a message the real text is in the thrown error's data read reject and reason off it
No handler for method a chain method called directly tunnel it through wallet_invokeMethod
non-BIP68-final not enough blocks have passed wait; the timelock is working
insufficient fee, rejecting replacement a previous attempt already reached the mempool check the explorer before retrying; it probably succeeded
window.humid is missing extension disabled, wrong profile, or page opened as file:// enable it and serve over http://
Import #0 "wbg": module is not an object or function a web-target wasm build overwrote the bundler-target pkg/ rebuild pkg/ with --target bundler
null pointer passed to rust a wasm handle used after a call consumed it build a fresh TxOut per call; reassign after every builder step
Expected a blinded address but got a non-blinded one Address.parse requires a confidential address use new Address(s) and check the network with isMainnet()

Talking to the Humid wallet

The injected provider is window.humid, with request({method, params}). Its content script matches file://, http:// and https://, with no pairing step.

It exposes only six CAIP-25/27 session methods (core/caip25/methods.ts): wallet_createSession, wallet_getSession, wallet_revokeSession, wallet_invokeMethod, wallet_addChain, wallet_switchChain. Anything else as a top-level method returns "No handler for method".

Every chain method is tunnelled:

await window.humid.request({
  method: "wallet_invokeMethod",
  params: {
    scope: "bip122:a771da8e52ee6ad581ed1e9a99825e5b",
    sessionId,
    request: { method: "processConfidentialTransaction", params },
  },
});

That scope is the Liquid testnet CAIP-2 id, from core/chains/liquid/domain/LiquidChain.ts.

Ten chain methods are registered (liquidRpcMethods.ts): getBalance, getIdentityPublicKey, getIdentitySharedKey, getUTXOs, getWalletDescriptor, processConfidentialTransaction, sendTransfer, signIdentity, signMessage, signPset. There is no getAddress, so a page cannot ask the wallet where to send funds.

Sessions and prompts

wallet_createSession shows a "Connect this dapp?" prompt unconditionally, and revokes any existing session for that origin first (extension-background/dapp-authorization/createDappAuthorization.ts). Calling it before each action destroys a working authorization and re-approves it every time. Attempt the method first and create a session only if the call is refused.

wallet_getSession is read-only and never prompts. {sessionScopes: {}} means no scopes are granted.

Sessions carry a 30-day TTL, but they live in the account model and require the wallet unlocked, so in practice they expire far sooner.

A separate second layer: each method has its own grant. The connect screen lists only the read-only methods the page requested, such as getWalletDescriptor, and ticks them by default. processConfidentialTransaction can't be pre-approved, so it never appears there (methodPolicyPresentation.ts): every call shows Humid's own "Perform a contract action?" confirmation, which is where you review and approve the transaction.

processConfidentialTransaction

Parameters, Zod-validated in packages/tx-manifest/src/request/validation.ts:

{ manifest, contractSources, action, params?, instance?, state?, broadcast? }
// broadcast defaults to false

contractSources keys must match the path the manifest names for the source, for example {"./allowance.simf": "<source text>"}.

instance is double-nested and state is not. normaliseInstance() reads raw.instance.fields:

instance: { instance: { fields: { BENEFICIARY_KEY, ALLOWANCE_AMOUNT, MIN_DISTANCE } } }
state:    { utxos: [{ txid, vout, utxo_type: "allowance" }] }

Single-nesting instance fails silently and resurfaces much later as "X is neither a parameter nor an argument of this action".

All parameter values are strings, including numbers.

The response is identical for a dry run and a broadcast:

{ broadcast, feeSats, transactionHex, txid, deployment? }

feeSats is a string, so parse it before doing arithmetic. deployment appears for actions with a create_instance block, that is Fund, and carries the instance fields the wallet computed, so a page need not echo its own form back to itself.

A refusal throws rather than returning a field, carrying reject and reason in the error data. So "refused" versus "built but not sent" is "did it throw" versus "did it resolve with broadcast: false".

transactionHex is the full raw transaction, which is enough to read the covenant address locally: a taproot scriptPubKey is the fixed pattern 22·5120·<32 bytes>, and bech32m encodes it with the tex HRP on testnet. Identify the output by shape rather than position, since the wallet chooses the order of a funding transaction's outputs.

Deriving keys and addresses from the wallet

invoke("getWalletDescriptor", {
  descriptorType: "publicWalletDescriptor",
  descriptorFormat: [{ format: "bip380-split-branches" }],
})

descriptors[0].branchDescriptors holds elwpkh([fingerprint/84h/1h/0h]xpub…/0/*) for branch: "external", which is plain BIP-84 segwit v0 rather than Taproot.

For BENEFICIARY_KEY: the xpub is already at m/84'/1'/0', so take two non-hardened CKDpub steps, /0 for the external chain then /0 for the first address, and drop the leading parity byte for the 64-hex x-only key. Decoding the receive address instead does not work, because P2WPKH commits to HASH160(pubkey).

For a receive address, which the wallet exposes no method for: P2WPKH of HASH160(pubkey), bech32 (witness v0, so bech32 rather than the bech32m used for the taproot covenant), with the tex HRP. The wallet's own receive screen may show the confidential tlq1… form of the same wallet.

How Humid treats a manifest

Humid never fetches a manifest. It arrives in the RPC call, validated only as being an object. There is no origin binding, no hash pinning, and no registry of known protocols.

manifest_version is read and never validated; the corpus tests delete the field to prove it is not load-bearing. Both the contract_templates and the older classes container are accepted.

document/registry.ts is a machine-readable list of which fields this build reads, shows, ignores, or has not implemented. It is the fastest way to check a manifest for fields that will be silently dropped.

attestation_version is unread. In the registry's own words it is "a signature slot that does not exist, and read by no implementation including the reference one".

Descriptions shown to the user come from the manifest and are untrusted text. Amounts do not: confirmation/provenance.ts tags every displayed value as verified, chain, computed or dapp, and the amounts a user sees are computed by the wallet.

The two manifest dialects here

examples/allowance-humid-only/txmanifest.json is manifest_version 0.1.0, for the wallet. examples/allowance/txmanifest.json is 0.3.0, for the CLI engine below. They describe the same contract.

examples/allowance/txmanifest.schema.json is the machine-checkable form of the 0.3.0 dialect only. ⚠️ Validating the 0.1.0 manifest against it reports exactly one error, on utxo_types.allowance:

Additional properties are not allowed ('confidential' was unexpected)

python3 -m jsonschema prints the whole utxo_types.allowance object before that message.

That is a dialect difference, not a fault. The schema permits confidential on an output but not on a utxo type, while Humid reads it there and every Humid fixture sets it. Any other error is worth investigating.

Installing and running Simplex

Two installations can collide. cargo install puts simplex in ~/.cargo/bin; simplexup puts it in ~/.simplex/bin and appends that to PATH. The cargo copy therefore normally wins, silently replacing the one simplexup manages. simplexup warns when it notices this.

Only the library is on crates.io. smplx-std, which the generated project depends on, is published; smplx-cli, which provides the simplex command, is not, so the binary has to come from the repository or the installer script.

The binary and the library must be the same release. simplex generates the code in src/artifacts/, so an older library rejects what a newer binary writes, failing with a type error about Program::new. simplex init writes the newest smplx-std from crates.io regardless of the binary's version, so a mismatch is easy to create by pinning only one of the two.

If git apply rejects a hunk of the Humid patch, that fix has landed upstream already. Apply the rest with git apply --reject, or check which of the two refusal strings the patch header quotes still appear in the Humid source and skip the hunk that no longer applies. The patch header describes both fixes well enough to redo by hand.

What simplex init leaves you to fix. It scaffolds tests/p2pk_test.rs whose body is todo!(), so it panics if left in place, and simf/p2pk.simf alongside it. It derives the package name by prefixing the directory, so allowance_example becomes the crate simplex_allowance_example, and a test importing the crate under its own name will not resolve until the package is renamed. It declares only smplx-std, while a test in the style of the Simplex repository's own examples also needs anyhow.

Chain access

Use https://blockstream.info/liquidtestnet/api from a browser, since it is the only Liquid Esplora mirror that sends CORS headers. https://liquid.network/liquidtestnet/api has gentler rate limits and is the better choice server-side.

blockstream.info refuses everything for minutes after a burst, so pace automatic refreshing against how long there is left to wait and stop when nothing is pending.

Sync the page's wallet through Waterfalls at https://waterfalls.liquidwebwallet.org/liquidtestnet/api, with a client created as new EsploraClient(network, url, true, 1, false), where true enables Waterfalls. A plain Esplora scan sends one request per address, 40 or more per sync, while Waterfalls returns the whole wallet's history in one request. The server sends CORS headers but isn't a full Esplora, so covenant lookups, the tip height and broadcasts still go to blockstream.info. The Waterfalls operator learns the wallet's addresses.

The faucet at liquidtestnet.com/api/faucet?address=…&action=lbtc doesn't send any CORS headers. A browser can dispatch the request with mode: "no-cors" and the faucet receives it, but the reply cannot be read. Confirm by checking the balance instead.

Other ways to drive the same contract

Each of these is useful for one thing. The page used in the HOWTO, examples/allowance-lwk/, is the one to copy for an application.

examples/allowance-humid-only/. The wallet half of that page with the library taken out: no wasm, no build step, no Rust, under 1,000 lines. It hands a manifest to Humid for every operation and keeps its own indexer. Worth copying when a page doesn't need to do anything a wallet cannot already do for it, and worth reading in any case, because it shows exactly which work stays yours once the contract itself is handed over. What it gives up is the ability to check anything: with no compiler it takes the covenant address from the funding transaction or from a handoff blob, on the benefactor's word. EXPLANATION.md covers where that matters.

simc and hal-simplicity. cargo install simplicityhl gives simc, which compiles a .simf with concrete parameters to a base64 program plus its CMR. cargo install hal-simplicity inspects programs and can build a PSET by hand. Neither path in the HOWTO needs them, but running simc once confirms the toolchain and contract are sound before you suspect anything else. The quickstart has the exact invocation.

tx-manifest-wallet. A prebuilt CLI engine that reads a manifest and runs the whole lifecycle, from stringhandler/txmanifest-wallet. Mind the hyphen: the repository is txmanifest-wallet and the binary is tx-manifest-wallet. Its use here is checking a manifest with no browser or extension involved:

tx-manifest-wallet run <manifest> <Action> --wallet w.json --params p.json
# answer N at the broadcast prompt

That exercises the real compile and witness path without spending anything, and it is the only path here that runs without a browser. It still prompts for a fee rate, so it needs a terminal. Use 0.3.0 or a later 0.3.x release: this engine checks manifest_version at parse time against its own FORMAT_VERSION, citing ELIP-205, unlike Humid, and a 0.2.x release rejects the 0.3.0 manifest outright. Its README says not to use it with real funds.