Page MenuHomePhabricator

[ecash-wallet] Add fluent API for building and broadcasting txs
ClosedPublic

Authored by bytesofman on Mar 19 2025, 04:27.

Details

Summary

We want a powerful and user-friendly API that exposes commonly used steps in tx processing.

Here we initialize such an API. The MVP includes basic support for SLP and ALP txs.

  • We can get the utxos required to build an arbitary tx with the exported selectUtxos function, which supports different selection strategies to support Postage protocol
  • We can build a tx (and get info about it) without broadcasting it, by calling wallet.tx(<instructions>).build().fee() or wallet.tx(<instructions>).build().size(); or the actual TxBuilder with wallet.tx(<instructions>).build().tx
  • We can build and broadcast a tx with one call with await wallet.tx(<instructions>).build().broadcast()
  • We can specify the outIdx of token outputs and have extensive validation against token spec about what works / does not work
  • We have included a regtest proof of concept for a postage tx
  • We have included regtest confirmed behavior for basic XEC, SLP, and ALP txs

This diff ended up piling in many features, as we had to build out a lot of this capability to properly evaluate the API. While the methods in this app do involve some complexity, it is greatly simplified compared to using ecash-lib directly, and the strong typing and validation enhance the developer experience.

From here,

  • We should add a websocket so the user does not need to remember to call await wallet.sync() before sending every tx
  • Support other token types
  • Support Agora actions
  • Add support for chained txs, i.e. await wallet.tx(<instructions>).build().broadcast() should "just work" even if the instructions are asking to send more token outputs than a single tx can hold, or more XEC outputs than the tx broadcast limit of 100kb will tolerate.
Test Plan

npm test

integration tests:
build node in build dir, then, from bitcoin-abc/modules/ecash-wallet/, BUILD_DIR="${PWD}/../../build" npm run integration-tests

Review API, suggest different names, shapes of objects, anything that should go in now before we start to add more features

Diff Detail

Repository
rABC Bitcoin ABC
Branch
wallet-txs-regtest
Lint
Lint Passed
Unit
No Test Coverage
Build Status
Buildable 33457
Build 66393: Build Diffecash-wallet-tests
Build 66392: arc lint + arc unit

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
modules/ecash-wallet/src/wallet.ts
45

Same with these consts, probably best to move them to some consts.ts file in ecash-lib

213

see below for a rationale

247

why I wouldn't move Wallet into ecash-lib: It uses ChronikClient, and that's a separate dependency

If we ever moved chronikclient into ecash-lib, we might also move Wallet

964

We'll need to change the design of this a bitβ€”for postage, the wallet often doesn't have enough satoshis (even if fee = 0)

Instead, we should try to select sats if possible, and if there's not enough, simply mark the WalletAction as having missing sats (instead of throwing an error).

For tokens, this is different; they always have to be sufficient. But maybe it also makes sense to return to the user how many tokens are missing, so the user of this library can cleanly display it: "You are missing 50 MUSD to pay for this" (without having to catch and parse the error)

1165

here, this will break any postage usage (see above)

1179

The issue with this relates to the issue above; if the user has 0 of a token, we will fail to infer its token type (leading to a cryptic error). But it should return that we have insufficient tokens.

Instead, I think it's better to require the user to specify the tokenType in the action. It's part of the data we get from the BIP70 SLP Payment Protocol, and it's easy to look up if needed (via e.g. via the UTXOs already in the wallet, the /token endpoint as a fallback).

1251

what a magnus opus

you might want to consider to move this out into its own file

1275

This will fail in unexpected ways if we fail to infer the token type

This revision now requires changes to proceed.Jun 3 2025, 10:46
bytesofman added inline comments.
modules/ecash-wallet/src/wallet.ts
964

We probably want utxo selection for postage to work differently than utxo selection for non-postage, since it's not about a shortage of sats so much as entirely disabling selection of fuel utxos. I think I can keep the prepareUtxos function if I use a param, but maybe preparePostageUtxos should be its own function.

For now, I have refactored to take gasless as an optional param (default false). If gasless, we do not look to add any fuel utxos. We do though, count satoshis from token utxos toward the required amount we need. I return postageSats as the sats missing to complete the tx.

1165

need some more thought here. Throwing the error is I think the best way to handle the case of user calling .build().broadcast() and this request failing due to insufficient utxos -- makes sense, we tried to do this thing, we could not do it for this reason.

Alt, we could always return something. Have some kind of ActionReturn interface with a success key, and return something like

{
success: false,
reason: "Insufficient utxos"
utxosInfo: PreparedUtxosInfo
}

if .build() or .build().broadcast() fails due to utxo issues.

1179

Refactored to parse this info from tokenActions instead of utxos

I thought would be able to simply drop the function entirely, but will probably still be a useful place to check things like valid / invalid combinations of token types (for example, the action of minting an SLP NFT will need special handling).

1251

πŸ™

and this is still the "MVP" version ... no handling for NFTs, no handling for agora, no handling for chained txs. Writing this tho was indeed illuminating as to why this kind of lib could offer disproportionate impact to users.

Could have its own file but it is currently related to preparedUtxos, getActionTotals, getTokenType, etc ... which are also all directly related to the wallet. I'm opent to alt org suggestions but I'm not sure how to optimize the taxonomy.

bytesofman marked 4 inline comments as done.

Refactor prepareUtxos for gasless prep, update tests, require specifying tokenType in tokenActions, move constants and interfaces to ecash-lib

tobias_ruck added inline comments.
modules/ecash-lib/src/index.ts
34 β†—(On Diff #54348)

move line this into payment.ts and change it to this, or move the file out of payment/

modules/ecash-lib/src/payment/action.ts
8 β†—(On Diff #54348)

make sure to add some doc comments

modules/ecash-lib/src/payment/index.ts
10 β†—(On Diff #54348)

i think it's more consistent to move exports from action.ts here

modules/ecash-lib/src/payment/output.ts
7 β†—(On Diff #54348)

Consider renaming this to PaymentOutput (probably just the Output type, but maybe the others too, dunno how it looks).

Then you can remove the commend above the export above

modules/ecash-lib/src/token/alp.ts
37 β†—(On Diff #54348)

ALP_TOKEN_TYPE_STANDARD should be sufficient (:

modules/ecash-wallet/src/wallet.ts
190 β†—(On Diff #54348)

might also make sense to remove this and just use map ad-hoc

194–198 β†—(On Diff #54348)
266 β†—(On Diff #54348)

What does this indicate exactly? That we don't have sufficient tokens, or insufficient sats?

If it's the first, you should make that clearer in the error.

If it's the latter, you should change the implementation such that it can select UTXOs without requiring the sats to be fulfilled.

544 β†—(On Diff #54348)

is the intended usage to catch the error and then add a change output? it might be smarter to return undefined, which is easier to handle

793 β†—(On Diff #54348)

I assume this will be removed before landing...

998 β†—(On Diff #54348)

this name needs some work, it sounds like an individual UTXO

e.g. "UtxosPreparation" or "UtxosResult", or a combination

This revision now requires changes to proceed.Jun 4 2025, 21:09
bytesofman added inline comments.
modules/ecash-wallet/src/wallet.ts
266 β†—(On Diff #54348)

still a bit of a WIP, not sure about the best way to handle this.

What does this indicate exactly? That we don't have sufficient tokens, or insufficient sats?

prepareUtxos returns an object that answers this. However now that prepareUtxos does not throw, if the user wants to build().broadcast() but can't because of a utxo issue ... we don't really know what to do about that.

I guess for the MVP, I could parse PreparedUtxosResult here and just throw if we don't have what we need. Then in the future we could decide how to handle the more granular cases.

did that for now -- lmk if that is not sufficiently extensible. I'm not really sure how to handle the postage stuff, but we should be sure that it isn't prevented by anything that gets in early.

544 β†—(On Diff #54348)

In this case, this (MVP) implementation only supports intentional BURN txs. So if we cannot burn 100% of utxos, we cannot intentionally BURN without also having a SEND. This means the currently implementation of ecash-wallet only allows exact burn amounts for SLP tokens. This is probably better than what we currently have, where Cashtab allows specific amounts, but does so by allowing intentional burns in this special case.

We may fail to getTokenUtxosWithExactAtoms for a number of reasons, all of which are useful for the user to know. So, undefined ... while easier to handle, we lose a lot of info.

It's a spec-related error, so we throw a specific error so the user knows why the tx is off spec.

Separate to this function -- at the moment this error would just be thrown if someone is trying to .build().broadcast() and this is the reason it is not possible. We could alternatively add a SEND action and a change output -- but on SLP this would require either logic to support intentional burns when we have this case, or logic for a chained tx. And on ALP, it's nice for the user to decide if he wants to SEND and BURN intentionally (possible, specify both actions), or BURN only (possible and a different thing, in which case ... must have the right inputs, and this error informs if he does not).

793 β†—(On Diff #54348)

😐

bytesofman marked 3 inline comments as done.

update and document types, throw explicit errors for missing tokens or missing sats, remove debug logs and legacy function, use .map instead of lazy wrapper function

tobias_ruck added inline comments.
modules/ecash-lib/src/token/alp.ts
22 β†—(On Diff #54485)

This is a breaking change, so we should bump the ecash-lib version to 4.0.0

modules/ecash-wallet/src/wallet.ts
163 β†—(On Diff #54485)

Seems more accurate

210 β†—(On Diff #54485)

I think it's better to have the constructor just take the fields of WalletAction as params (probably as obj), and put them into the fields, and then to move the current logic of this function into a public static function fromAction(wallet, action)

215 β†—(On Diff #54485)

Anything wrong with just having preparedUtxosResult here? We can unwrap it in build, then there's no need for the TODO in the if below

248 β†—(On Diff #54485)
270 β†—(On Diff #54485)

also note that I think it's best to move these into prepareUtxos (or selectUtxos)

359 β†—(On Diff #54485)

booleans should be questions IMO

362 β†—(On Diff #54485)

seems like a refactor relic?

365 β†—(On Diff #54485)

this duplicates the same code from aboveβ€”is there no way to wrap this in a function? it can be internal to build if you need to reference the variabled; of course great would be to have a private method too

458 β†—(On Diff #54485)

...or you can add an error field here to add a human-readable error message

786 β†—(On Diff #54485)

I'd make all fields in here non-optional, except utxos:

tokens can be the empty map if no tokens are missing
missingSats can be 0 if no sats are missing

790 β†—(On Diff #54485)

I think this would be clearer, unless I misunderstand (I'm not sure exactly if this only contains exactly the missing tokens)

794 β†—(On Diff #54485)

seems fine to use the same variable for missing sats for both postage, and the "non-gasless` case

797 β†—(On Diff #54485)

thoughts on this name? this way, a SatsSelection (or SatsSelectionMode) enum with different selection modes makes more sense

807 β†—(On Diff #54485)

it seems like this is never called above; also probably better to turn the params into an object ({ action }: { action: payment.Action } etc.), then it's clearer what the gasless means.

Additionally, it's probably best to set this to an enum, in case we need to add another strategy; there's already 3 strategies (SatsSelection) that could be used:

  • RequireSats (default): Must select enough sats to cover for outputs, otherwise err (basically every wallet ever)
  • AttemptSats: Try to cover sats, otherwise return UTXOs which cover less than asked for (probably the best for most wallets)
  • NoSats (or AvoidSats): Don't add sats, even if they're available (this is actually what we do at Pay2Stay/TixTown).

Instead of throwing an error for RequireSats, just return success: false.

Also, it's probably best to add a errors field if you want, then you don't need to construct the error in build (which seems leaky).

1021 β†—(On Diff #54485)

isn't this what you want? I think it should be set if gasless is true or not

1023 β†—(On Diff #54485)

wouldn't it be better/simpler to always set this (at the beginning of the function), regardless of whether we have enough or not?

also, shouldn't this be the sats requested, not the sats missing?

1034 β†—(On Diff #54485)

NB: This is true for now; if there was another ALP version, we'd be able to support two token types per action.

Also, in the future, we can support multiple txs if there's multiple token types.

Maybe you can add this into the doc comment to clarify.

This revision now requires changes to proceed.Jun 23 2025, 21:38
bytesofman added inline comments.
modules/ecash-wallet/src/wallet.ts
270 β†—(On Diff #54485)

actually based on how the msg is constructed, this is what we want. we keep the space at the front to support listing multiple shortcomings

365 β†—(On Diff #54485)

used a private method

...approach still seems kind of janky. just recently cleaned up a bug for this in Cashtab so important to test this. Can get complex at the borderline where we have enough sats in token utxos to cover output sats but not the fee -- which is surprisingly common in token wallets sending token txs.

458 β†—(On Diff #54485)

added. still unsure best way to communicate this to a user.

We could have .build() return something with a {success: boolean}, but I'm not sure if that's better than current behavior (build() returns a BuiltTx or throws). If we did this, then we only throw on build().broadcast()

alt, we could add a checkBuild() function that does not throw and instead returns {success: boolean; builtTx: BuiltTx}

794 β†—(On Diff #54485)

makes sense

807 β†—(On Diff #54485)

Could you expand on this one? I'm not totally sure what we're looking to implement, still unfamiliar with how BIP70 is intended to work in practice. Mb easier to get into now that the other changes are taken care of (pending next update)

bytesofman marked 4 inline comments as done.

shape improvements suggested in comments, name improvements

rebase, include suggested sats selection strategies

add support and regtest for postage tx

gotta do better than the handwavy fee selection for postage

try...catch for fuel utxo adds to postage txs

tobias_ruck added inline comments.
modules/ecash-lib/src/token/common.ts
63–66 β†—(On Diff #54843)

redundant

This revision is now accepted and ready to land.Jul 18 2025, 17:17

rebase, fix nit, back up for CI check