-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Transaction builder #34
Conversation
To view the generated documentation, you can install elm-doc-preview and run it from the project home. |
An alternative way of building might be the following: type alias Tx =
{ intents : List TxIntent
, other : List TxOther
}
finalizeTx : List Tx -> ChangeStrategy -> OtherLocalContextStuff -> Result String Transaction
type alias ChangeStrategy = List ( Output, Value ) -> ChangeReallocation Where Then I think it would be easier to build a Tx in small parts and join them together. Something like this? nftMintTx =
{ intents = [ mintNft, transferToMe ]
, other = [ nftMetadata ]
}
swapTx =
{ intents = [ transferFromMe, sendToScript ]
, other = [ swapMetadata ]
}
finalizeTx [ nftMintTx, swapTx ] myChangeStrat otherContextStuff No idea if that would be any better, or even more verbose and annoying ... |
One key design decision in this PR is the For example, we might add a transfer, and say to send the change back to us, but while doing something else (e.g. a swap), the change in that utxo might have been useful to use instead of sending it back and having to look for another utxo for input for the swap. That being said, it made the last example (spending half of a utxo in a script) more challenging because we want to send the rest of that utxo back to the same script address with the same metadata. In practice, I knew the amount that had to be sent, so I could easily build the change to send back 1 ada to the contract. But what if I wanted to have it automatically figured out? I’d have to look through the whole list of consumed outputs to find the one of the script, get that amount and use it. It’s doable, just adds much more verbosity, meaning potentially bugs? I’m wondering if a better way, would be to add "partial change" handling in the spending intents, especially the one from plutus script. Something like this: spendFromPlutusScript :
PlutusScriptSource
-> ScriptUtxoSelection
-> Value
-> ChangeStrat -- new
-> Tx WIP
-> Tx WIP Or maybe, I suspect this will only be useful if you manually select the spent utxos in the first place, but I don’t know. In that case, it could be only an addition to the manual selection type: type ScriptUtxoSelection
= AutoScriptUtxoSelection ({ ref : OutputReference, utxo : Output } -> Maybe { redeemer : Data })
| ManualScriptUtxoSelection
{ selection :
List
{ ref : OutputReference
, utxo : Output
, redeemer : Data
}
, partialChange : ChangeStrat -- new
} No idea if that’s better in the grand scheme of composing transactions. |
Another remark. I want to be able to accommodate a design pattern for composable transactions that rely on the position/ordering of inputs, with markers provided by the redeemer. A more complete explanation of what I mean is given by comments cardano-foundation/CIPs#758 (comment) by @colll78 and cardano-foundation/CIPs#758 (comment) by @fallen-icarus. So maybe I need to look a bit more into these contracts. Adding markers for spent script inputs that can be used in redeemers data seems like a relevant use case. |
Also another reference to keep in mind: mlab’s new purescript transaction builder https://github.com/mlabs-haskell/purescript-cardano-transaction-builder |
@mpizenberg consider https://github.com/klntsky/cardano-purescript altogether. the builder is useless by itself, as it is just a small dsl+interpreter |
thanks @klntsky . From what I read, the main missing part from the builder dsl is the balancing part which lives in https://github.com/Plutonomicon/cardano-transaction-lib right? I don’t have time to look at all the links in your awesome list ^^ |
@mpizenberg yes |
@klntsky I didn’t find a way with purescript-cardano-transaction-builder to build transactions following indexing patterns, like the one-to-one input/output indexer in the redeemer pattern described in anastasia labs readme here: https://github.com/Anastasia-Labs/aiken-design-patterns?tab=readme-ov-file#singular-utxo-indexer Do you have an idea of how that would be possible? |
Another relevant source used by Anastasia Labs https://github.com/j-mueller/sc-tools/blob/main/src/base/lib/Convex/BuildTx.hs |
If control over indices is needed, best to build the transaction in plain PS |
It's not just control over the indexes that is needed, it is the ability to construct and use redeemers that depend on the indices of inputs (which can change at balancing). The redeemer should accurately reflect their correct indices as they appear in the final tx (after balancing). |
So I’ve discussed a bit with Matthias and it seems like a function-heavy API is a bit too intense, so probably a DSL approach is easier to grasp. The change control also seems mentally taxing. I liked that it enabled making sure the user is building a balanced transaction though. So I’ll probably move to a balancing approach like most others, but still not 100% sold on this. Anyway. Taking some feedback into account, it could make sense to move to something very similar to purescript DSL approach, but with two key differences. (1) Enabling intents based on amounts and addresses, which avoids having to select exact output references or create exact outputs for things that are fungible (so mainly assets at regular addresses or native script addresses). (2) Enabling redeemer construction with a function that depends on the selected inputs and created outputs. Finalizing the Tx will require a multi-pass solver anyway, so I see no harm in helping dynamic redeemer constructions, as long as we protect from infinite loops in the solver. Here is what it would look like (only describing the type Intent
= Spend SpendSource
| ...
type SpendSource
= AutoSelectFrom Address Value
| FromUtxo { input : OutputReference, spendWitness : SpendWitness }
type SpendWitness
= NoSpendWitness
| NativeWitness (ScriptWitness NativeScript)
| PlutusWitness
{ scriptWitness : ScriptWitness PlutusScript
, datumWitness : DatumWitness
, redeemerDatum : RedeemerDatum
}
type RedeemerDatum
= FixedRedeemer Data
-- This should enable easier indexing pattern
| RedeemerFunction (RedeemerContext -> Data)
type alias RedeemerContext =
{ referenceInputs : List OutputReference
, spentInputs : List OutputReference
, createdOutputs : List Output
} |
In this new API, including required signers under |
Ok, I’ve refined it a bit more. Still very close to the purescript DSL but slightly different. type TxIntent
= SendToAutoCreate Address Value
| SendToOutput (InputsOutputs -> Output)
-- Spending assets from somewhere
| SpendFromAutoSelect Address Value
| SpendFromUtxo
{ input : OutputReference
, spendWitness : SpendWitness
}
-- Minting / burning assets
| MintBurn
{ policyId : Bytes CredentialHash
, assets : BytesMap AssetName Integer
, credentialWitness : CredentialWitness
}
-- Issuing certificates
| IssueCertificate Todo
-- Withdrawing rewards
| WithdrawRewards Todo
type alias InputsOutputs =
{ referenceInputs : List OutputReference
, spentInputs : List OutputReference
, createdOutputs : List Output
}
type CredentialWitness
= NativeScriptCredential (ScriptWitness NativeScript)
| PlutusScriptCredential
{ scriptWitness : ScriptWitness PlutusScript
, redeemerData : InputsOutputs -> Data
}
type SpendWitness
= NoSpendWitness
| NativeWitness (ScriptWitness NativeScript)
| PlutusWitness
{ scriptWitness : ScriptWitness PlutusScript
, datumWitness : Maybe DatumWitness
, redeemerData : InputsOutputs -> Data
}
type ScriptWitness a
= ScriptValue a
| ScriptReference OutputReference
type DatumWitness
= DatumValue Data
| DatumReference OutputReference |
@keyan-m something like this: type ScriptWitness a
= ScriptValue
{ script : a
, requiredSigners : List (Bytes CredentialHash)
}
| ScriptReference
{ ref : OutputReference
, requiredSigners : List (Bytes CredentialHash)
} |
Or maybe better, I add it the plutus variants of the credential and spend witnesses to avoid the incorrect state of having required signers for a native script. type CredentialWitness
= NativeScriptCredential (ScriptWitness NativeScript)
| PlutusScriptCredential
{ scriptWitness : ScriptWitness PlutusScript
, redeemerData : InputsOutputs -> Data
, requiredSigners : List (Bytes CredentialHash) -- new
}
type SpendWitness
= NoSpendWitness
| NativeWitness (ScriptWitness NativeScript)
| PlutusWitness
{ scriptWitness : ScriptWitness PlutusScript
, datumWitness : Maybe DatumWitness
, redeemerData : InputsOutputs -> Data
, requiredSigners : List (Bytes CredentialHash) -- new
} |
This PR is a WIP, but ready for a first pass of reviews.
Here is a screenshot to have a quick overview of the API without having to look at the changes or using elm-doc-preview. (Might not be up-to-date with the latest state of this PR)
I’ve started by drafting all the types and writing the documentation, so that reading the documentation should tell you exactly how the building would be done in a majority of cases. I’ve also added at the end of the files some non-exposed examples in code, not only in documentation, so that the compiler would check that it makes sense.
The base idea is that there is a
Tx status
type, that should contain all necessary information while building the transaction. Thestatus
type parameter is a phantom type to record progress in the building and impose some very light constraints. I could have gone more type-heavy with status to have even more precise constraints, but I don’t think it’s worth it before having first some feedback.Basically, the construction of the Tx goes as follows:
So basically, we have the following steps:
Nothing is implemented, there are basically
Debug.todo
everywhere, but it typechecks. Also, the finalization step I haven’t thought too much about yet. Wondering if that can stay a pure function, or eventually return a Task of some sort, that will then be sent for signature.