diff --git a/bot-plutus-interface.cabal b/bot-plutus-interface.cabal index 92d88fbb..152cdc73 100644 --- a/bot-plutus-interface.cabal +++ b/bot-plutus-interface.cabal @@ -85,6 +85,7 @@ library BotPlutusInterface.Types BotPlutusInterface.UtxoParser BotPlutusInterface.Server + BotPlutusInterface.Helpers build-depends: , aeson ^>=1.5.0.0 , attoparsec >=0.13.2.2 diff --git a/examples/plutus-game/app/Main.hs b/examples/plutus-game/app/Main.hs index 25e306b3..d4aac89f 100644 --- a/examples/plutus-game/app/Main.hs +++ b/examples/plutus-game/app/Main.hs @@ -61,13 +61,14 @@ main = do , pcTipPollingInterval = 10_000_000 , pcSlotConfig = def , pcOwnPubKeyHash = "0f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546" + , pcOwnStakePubKeyHash = Nothing , pcScriptFileDir = "./scripts" , pcSigningKeyFileDir = "./signing-keys" , pcTxFileDir = "./txs" , pcDryRun = True , pcLogLevel = Debug , pcProtocolParamsFile = "./protocol.json" - , pcForceBudget = Just (1000, 1000) + , pcForceBudget = Just (9_000_000_000, 15_000_000) , pcEnableTxEndpoint = True } BotPlutusInterface.runPAB @GameContracts pabConf diff --git a/examples/plutus-game/guess.sh b/examples/plutus-game/guess.sh index 179c4e80..2c9ef3bb 100755 --- a/examples/plutus-game/guess.sh +++ b/examples/plutus-game/guess.sh @@ -6,7 +6,7 @@ CONTRACT_INST_ID=$(curl --location --request POST 'localhost:9080/api/contract/a "caID": { "tag": "Guess", "contents": { - "guessGameId": 2, + "guessGameId": 3, "guessSecret": "secret" } } diff --git a/examples/plutus-game/lock.sh b/examples/plutus-game/lock.sh index 9afb12ab..e151f006 100755 --- a/examples/plutus-game/lock.sh +++ b/examples/plutus-game/lock.sh @@ -6,7 +6,7 @@ CONTRACT_INST_ID=$(curl --location --request POST 'localhost:9080/api/contract/a "caID": { "tag": "Lock", "contents": { - "lockGameId": 2, + "lockGameId": 3, "lockAmount": 1000000, "lockSecret": "secret" } diff --git a/examples/plutus-game/plutus-game.cabal b/examples/plutus-game/plutus-game.cabal index 109069f6..c4b85789 100644 --- a/examples/plutus-game/plutus-game.cabal +++ b/examples/plutus-game/plutus-game.cabal @@ -81,6 +81,7 @@ library , cardano-crypto , cardano-ledger-alonzo , containers + , bot-plutus-interface , data-default , data-default-class , directory diff --git a/examples/plutus-game/protocol.json b/examples/plutus-game/protocol.json index daa1b5f1..a1718e8e 100644 --- a/examples/plutus-game/protocol.json +++ b/examples/plutus-game/protocol.json @@ -1,208 +1,208 @@ { - "txFeePerByte": 44, + "maxValueSize": 5000, "minUTxOValue": null, - "decentralization": 0.7, - "utxoCostPerWord": 34482, - "stakePoolDeposit": 0, + "minPoolCost": 340000000, + "monetaryExpansion": 3.0e-3, + "stakeAddressDeposit": 2000000, + "txFeeFixed": 155381, "poolRetireMaxEpoch": 18, - "extraPraosEntropy": null, - "collateralPercentage": 150, - "stakePoolTargetNum": 100, - "maxBlockBodySize": 65536, - "minPoolCost": 0, - "maxTxSize": 16384, - "treasuryCut": 0.1, + "stakePoolDeposit": 500000000, "maxBlockExecutionUnits": { - "memory": 50000000, + "memory": 80000000, "steps": 40000000000 }, - "maxCollateralInputs": 3, - "maxValueSize": 5000, + "extraPraosEntropy": null, + "stakePoolTargetNum": 500, "maxBlockHeaderSize": 1100, - "maxTxExecutionUnits": { - "memory": 10000000, - "steps": 10000000000 + "maxCollateralInputs": 3, + "txFeePerByte": 44, + "treasuryCut": 0.2, + "protocolVersion": { + "minor": 0, + "major": 6 }, + "collateralPercentage": 150, + "poolPledgeInfluence": 0.3, "costModels": { "PlutusScriptV1": { - "cekConstCost-exBudgetMemory": 100, - "unBData-cpu-arguments": 150000, - "divideInteger-memory-arguments-minimum": 1, - "nullList-cpu-arguments": 150000, - "cekDelayCost-exBudgetMemory": 100, - "appendByteString-cpu-arguments-slope": 621, - "sha2_256-memory-arguments": 4, - "multiplyInteger-cpu-arguments-intercept": 61516, - "iData-cpu-arguments": 150000, - "equalsString-cpu-arguments-intercept": 150000, - "trace-cpu-arguments": 150000, - "lessThanEqualsByteString-cpu-arguments-intercept": 103599, - "encodeUtf8-cpu-arguments-slope": 1000, + "mapData-memory-arguments": 32, + "lessThanInteger-memory-arguments": 1, + "sha3_256-cpu-arguments-slope": 82363, + "bData-cpu-arguments": 150000, + "equalsByteString-cpu-arguments-intercept": 112536, "equalsString-cpu-arguments-constant": 1000, - "blake2b-cpu-arguments-slope": 29175, + "modInteger-memory-arguments-slope": 1, + "equalsInteger-memory-arguments": 1, + "trace-cpu-arguments": 150000, + "iData-cpu-arguments": 150000, + "equalsByteString-memory-arguments": 1, + "unIData-memory-arguments": 32, "consByteString-memory-arguments-intercept": 0, - "headList-cpu-arguments": 150000, - "listData-cpu-arguments": 150000, - "divideInteger-cpu-arguments-model-arguments-slope": 118, - "divideInteger-memory-arguments-slope": 1, - "bData-cpu-arguments": 150000, - "chooseData-memory-arguments": 32, - "cekBuiltinCost-exBudgetCPU": 29773, - "mkNilData-memory-arguments": 32, - "equalsInteger-cpu-arguments-intercept": 136542, - "lengthOfByteString-cpu-arguments": 150000, - "subtractInteger-cpu-arguments-slope": 0, - "unIData-cpu-arguments": 150000, - "sliceByteString-cpu-arguments-slope": 5000, - "unMapData-cpu-arguments": 150000, - "modInteger-cpu-arguments-model-arguments-slope": 118, + "cekLamCost-exBudgetCPU": 29773, + "indexByteString-cpu-arguments": 150000, + "cekStartupCost-exBudgetMemory": 100, + "listData-memory-arguments": 32, + "divideInteger-cpu-arguments-constant": 148000, "lessThanInteger-cpu-arguments-intercept": 179690, + "verifySignature-cpu-arguments-slope": 1, "appendString-memory-arguments-intercept": 0, - "mkCons-cpu-arguments": 150000, - "sha3_256-cpu-arguments-slope": 82363, - "ifThenElse-cpu-arguments": 1, - "mkNilPairData-cpu-arguments": 150000, + "equalsString-cpu-arguments-slope": 1000, + "blake2b-cpu-arguments-intercept": 2477736, + "encodeUtf8-cpu-arguments-slope": 1000, + "mapData-cpu-arguments": 150000, + "equalsByteString-cpu-arguments-slope": 247, + "multiplyInteger-cpu-arguments-intercept": 61516, + "cekStartupCost-exBudgetCPU": 100, + "sndPair-memory-arguments": 32, + "sha3_256-cpu-arguments-intercept": 0, + "addInteger-cpu-arguments-slope": 0, "constrData-memory-arguments": 32, + "divideInteger-memory-arguments-intercept": 0, + "cekForceCost-exBudgetCPU": 29773, + "equalsByteString-cpu-arguments-constant": 150000, + "modInteger-cpu-arguments-model-arguments-intercept": 425507, + "sliceByteString-memory-arguments-slope": 1, + "equalsString-memory-arguments": 1, + "cekLamCost-exBudgetMemory": 100, "lessThanEqualsInteger-cpu-arguments-intercept": 145276, - "addInteger-memory-arguments-slope": 1, - "chooseList-memory-arguments": 32, - "equalsData-memory-arguments": 1, - "decodeUtf8-cpu-arguments-intercept": 150000, - "bData-memory-arguments": 32, - "lessThanByteString-cpu-arguments-slope": 248, - "listData-memory-arguments": 32, + "quotientInteger-memory-arguments-minimum": 1, "consByteString-cpu-arguments-intercept": 150000, - "headList-memory-arguments": 32, - "subtractInteger-memory-arguments-slope": 1, - "appendByteString-memory-arguments-intercept": 0, - "unIData-memory-arguments": 32, - "remainderInteger-memory-arguments-minimum": 1, - "lengthOfByteString-memory-arguments": 4, - "encodeUtf8-memory-arguments-intercept": 0, - "cekStartupCost-exBudgetCPU": 100, - "remainderInteger-memory-arguments-slope": 1, - "multiplyInteger-memory-arguments-intercept": 0, - "cekForceCost-exBudgetCPU": 29773, - "unListData-memory-arguments": 32, - "sha2_256-cpu-arguments-slope": 29175, - "indexByteString-memory-arguments": 1, - "equalsInteger-memory-arguments": 1, - "remainderInteger-cpu-arguments-model-arguments-slope": 118, + "appendByteString-memory-arguments-slope": 1, + "lessThanByteString-cpu-arguments-slope": 248, + "lessThanByteString-memory-arguments": 1, + "multiplyInteger-cpu-arguments-slope": 11218, "cekVarCost-exBudgetCPU": 29773, - "lessThanEqualsInteger-cpu-arguments-slope": 1366, - "addInteger-memory-arguments-intercept": 1, - "sndPair-cpu-arguments": 150000, - "lessThanInteger-memory-arguments": 1, - "cekLamCost-exBudgetCPU": 29773, - "chooseUnit-cpu-arguments": 150000, - "decodeUtf8-cpu-arguments-slope": 1000, - "fstPair-cpu-arguments": 150000, - "quotientInteger-memory-arguments-minimum": 1, - "lessThanEqualsInteger-memory-arguments": 1, + "cekDelayCost-exBudgetMemory": 100, + "blake2b-cpu-arguments-slope": 29175, + "mkNilData-cpu-arguments": 150000, + "appendByteString-cpu-arguments-slope": 621, + "appendString-memory-arguments-slope": 1, + "lessThanInteger-cpu-arguments-slope": 497, "chooseUnit-memory-arguments": 32, - "fstPair-memory-arguments": 32, + "divideInteger-cpu-arguments-model-arguments-slope": 118, + "decodeUtf8-cpu-arguments-slope": 1000, + "chooseData-cpu-arguments": 150000, + "verifySignature-cpu-arguments-intercept": 3345831, + "modInteger-memory-arguments-minimum": 1, + "lessThanEqualsByteString-memory-arguments": 1, "quotientInteger-cpu-arguments-constant": 148000, - "mapData-cpu-arguments": 150000, - "unConstrData-cpu-arguments": 150000, - "mkPairData-cpu-arguments": 150000, - "sndPair-memory-arguments": 32, - "decodeUtf8-memory-arguments-slope": 8, - "equalsData-cpu-arguments-intercept": 150000, - "addInteger-cpu-arguments-intercept": 197209, - "modInteger-memory-arguments-intercept": 0, - "cekStartupCost-exBudgetMemory": 100, - "divideInteger-cpu-arguments-model-arguments-intercept": 425507, - "divideInteger-memory-arguments-intercept": 0, - "cekVarCost-exBudgetMemory": 100, "consByteString-memory-arguments-slope": 1, - "cekForceCost-exBudgetMemory": 100, - "unListData-cpu-arguments": 150000, - "subtractInteger-cpu-arguments-intercept": 197209, - "indexByteString-cpu-arguments": 150000, - "equalsInteger-cpu-arguments-slope": 1326, - "lessThanByteString-memory-arguments": 1, - "blake2b-cpu-arguments-intercept": 2477736, - "encodeUtf8-cpu-arguments-intercept": 150000, - "multiplyInteger-cpu-arguments-slope": 11218, - "tailList-cpu-arguments": 150000, + "tailList-memory-arguments": 32, + "divideInteger-cpu-arguments-model-arguments-intercept": 425507, + "decodeUtf8-cpu-arguments-intercept": 150000, + "lessThanEqualsInteger-memory-arguments": 1, "appendByteString-cpu-arguments-intercept": 396231, - "equalsString-cpu-arguments-slope": 1000, - "lessThanEqualsByteString-cpu-arguments-slope": 248, - "remainderInteger-cpu-arguments-constant": 148000, - "chooseList-cpu-arguments": 150000, - "equalsByteString-memory-arguments": 1, - "constrData-cpu-arguments": 150000, - "cekApplyCost-exBudgetCPU": 29773, - "equalsData-cpu-arguments-slope": 10000, - "decodeUtf8-memory-arguments-intercept": 0, - "modInteger-memory-arguments-slope": 1, - "addInteger-cpu-arguments-slope": 0, - "appendString-cpu-arguments-intercept": 150000, - "quotientInteger-cpu-arguments-model-arguments-slope": 118, "unMapData-memory-arguments": 32, + "chooseList-cpu-arguments": 150000, + "divideInteger-memory-arguments-minimum": 1, + "unListData-memory-arguments": 32, + "remainderInteger-cpu-arguments-constant": 148000, + "addInteger-memory-arguments-slope": 1, + "sha3_256-memory-arguments": 4, + "lessThanByteString-cpu-arguments-intercept": 103599, + "modInteger-cpu-arguments-constant": 148000, + "lessThanEqualsInteger-cpu-arguments-slope": 1366, + "appendByteString-memory-arguments-intercept": 0, + "listData-cpu-arguments": 150000, + "ifThenElse-memory-arguments": 1, "cekApplyCost-exBudgetMemory": 100, - "quotientInteger-memory-arguments-slope": 1, + "sliceByteString-memory-arguments-intercept": 0, + "unIData-cpu-arguments": 150000, + "modInteger-cpu-arguments-model-arguments-slope": 118, + "equalsData-cpu-arguments-intercept": 150000, "mkNilPairData-memory-arguments": 32, - "ifThenElse-memory-arguments": 1, - "equalsByteString-cpu-arguments-slope": 247, - "sliceByteString-memory-arguments-slope": 1, - "sha3_256-memory-arguments": 4, - "mkCons-memory-arguments": 32, - "verifySignature-cpu-arguments-intercept": 3345831, - "cekBuiltinCost-exBudgetMemory": 100, - "remainderInteger-memory-arguments-intercept": 0, - "lessThanEqualsByteString-memory-arguments": 1, - "mkNilData-cpu-arguments": 150000, - "equalsString-memory-arguments": 1, - "chooseData-cpu-arguments": 150000, - "remainderInteger-cpu-arguments-model-arguments-intercept": 425507, - "tailList-memory-arguments": 32, - "sha2_256-cpu-arguments-intercept": 2477736, + "cekConstCost-exBudgetCPU": 29773, + "indexByteString-memory-arguments": 1, + "blake2b-memory-arguments": 4, + "lessThanEqualsByteString-cpu-arguments-slope": 248, + "cekDelayCost-exBudgetCPU": 29773, "multiplyInteger-memory-arguments-slope": 1, + "remainderInteger-memory-arguments-slope": 1, + "subtractInteger-cpu-arguments-slope": 0, "iData-memory-arguments": 32, - "divideInteger-cpu-arguments-constant": 148000, - "cekDelayCost-exBudgetCPU": 29773, - "encodeUtf8-memory-arguments-slope": 8, - "subtractInteger-memory-arguments-intercept": 1, - "nullList-memory-arguments": 32, - "lessThanByteString-cpu-arguments-intercept": 103599, - "appendByteString-memory-arguments-slope": 1, - "blake2b-memory-arguments": 4, - "unBData-memory-arguments": 32, - "cekConstCost-exBudgetCPU": 29773, - "consByteString-cpu-arguments-slope": 1000, + "cekBuiltinCost-exBudgetCPU": 29773, + "mkNilData-memory-arguments": 32, + "cekForceCost-exBudgetMemory": 100, "trace-memory-arguments": 32, - "quotientInteger-memory-arguments-intercept": 0, - "mapData-memory-arguments": 32, - "verifySignature-cpu-arguments-slope": 1, + "encodeUtf8-cpu-arguments-intercept": 150000, + "sha2_256-cpu-arguments-intercept": 2477736, + "headList-memory-arguments": 32, + "unBData-cpu-arguments": 150000, + "remainderInteger-memory-arguments-minimum": 1, + "unMapData-cpu-arguments": 150000, + "sha2_256-cpu-arguments-slope": 29175, + "modInteger-memory-arguments-intercept": 0, + "ifThenElse-cpu-arguments": 1, + "tailList-cpu-arguments": 150000, + "multiplyInteger-memory-arguments-intercept": 0, + "remainderInteger-memory-arguments-intercept": 0, + "consByteString-cpu-arguments-slope": 1000, + "lengthOfByteString-memory-arguments": 4, + "fstPair-memory-arguments": 32, + "mkPairData-cpu-arguments": 150000, + "appendString-cpu-arguments-intercept": 150000, + "verifySignature-memory-arguments": 1, + "sliceByteString-cpu-arguments-intercept": 150000, + "equalsData-cpu-arguments-slope": 10000, + "lessThanEqualsByteString-cpu-arguments-intercept": 103599, + "chooseList-memory-arguments": 32, + "nullList-memory-arguments": 32, + "unListData-cpu-arguments": 150000, + "equalsData-memory-arguments": 1, + "quotientInteger-cpu-arguments-model-arguments-slope": 118, + "sha2_256-memory-arguments": 4, "quotientInteger-cpu-arguments-model-arguments-intercept": 425507, - "modInteger-cpu-arguments-constant": 148000, + "encodeUtf8-memory-arguments-intercept": 0, + "quotientInteger-memory-arguments-slope": 1, + "unConstrData-cpu-arguments": 150000, + "sliceByteString-cpu-arguments-slope": 5000, + "cekBuiltinCost-exBudgetMemory": 100, + "equalsInteger-cpu-arguments-slope": 1326, + "subtractInteger-memory-arguments-slope": 1, + "mkCons-cpu-arguments": 150000, + "chooseUnit-cpu-arguments": 150000, + "chooseData-memory-arguments": 32, + "bData-memory-arguments": 32, "appendString-cpu-arguments-slope": 1000, + "decodeUtf8-memory-arguments-slope": 8, + "fstPair-cpu-arguments": 150000, "unConstrData-memory-arguments": 32, + "sndPair-cpu-arguments": 150000, + "addInteger-memory-arguments-intercept": 1, + "cekConstCost-exBudgetMemory": 100, + "remainderInteger-cpu-arguments-model-arguments-intercept": 425507, + "equalsString-cpu-arguments-intercept": 150000, + "mkCons-memory-arguments": 32, "mkPairData-memory-arguments": 32, - "equalsByteString-cpu-arguments-constant": 150000, - "equalsByteString-cpu-arguments-intercept": 112536, - "sliceByteString-memory-arguments-intercept": 0, - "lessThanInteger-cpu-arguments-slope": 497, - "verifySignature-memory-arguments": 1, - "cekLamCost-exBudgetMemory": 100, - "sliceByteString-cpu-arguments-intercept": 150000, - "modInteger-cpu-arguments-model-arguments-intercept": 425507, - "modInteger-memory-arguments-minimum": 1, - "appendString-memory-arguments-slope": 1, - "sha3_256-cpu-arguments-intercept": 0 + "lengthOfByteString-cpu-arguments": 150000, + "remainderInteger-cpu-arguments-model-arguments-slope": 118, + "cekApplyCost-exBudgetCPU": 29773, + "constrData-cpu-arguments": 150000, + "nullList-cpu-arguments": 150000, + "headList-cpu-arguments": 150000, + "decodeUtf8-memory-arguments-intercept": 0, + "subtractInteger-cpu-arguments-intercept": 197209, + "subtractInteger-memory-arguments-intercept": 1, + "encodeUtf8-memory-arguments-slope": 8, + "equalsInteger-cpu-arguments-intercept": 136542, + "quotientInteger-memory-arguments-intercept": 0, + "cekVarCost-exBudgetMemory": 100, + "unBData-memory-arguments": 32, + "addInteger-cpu-arguments-intercept": 197209, + "mkNilPairData-cpu-arguments": 150000, + "divideInteger-memory-arguments-slope": 1 } }, - "protocolVersion": { - "minor": 0, - "major": 5 + "maxTxExecutionUnits": { + "memory": 16000000, + "steps": 10000000000 }, - "txFeeFixed": 155381, - "stakeAddressDeposit": 0, - "monetaryExpansion": 0.1, - "poolPledgeInfluence": 0, "executionUnitPrices": { "priceSteps": 7.21e-5, "priceMemory": 5.77e-2 - } + }, + "decentralization": 0, + "utxoCostPerWord": 34482, + "maxTxSize": 16384, + "maxBlockBodySize": 98304 } \ No newline at end of file diff --git a/examples/plutus-nft/app/Main.hs b/examples/plutus-nft/app/Main.hs index 98d1e907..e6d1ebf7 100644 --- a/examples/plutus-nft/app/Main.hs +++ b/examples/plutus-nft/app/Main.hs @@ -61,6 +61,7 @@ main = do , pcTipPollingInterval = 10_000_000 , pcSlotConfig = def , pcOwnPubKeyHash = "0f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546" + , pcOwnStakePubKeyHash = Nothing , pcScriptFileDir = "./scripts" , pcSigningKeyFileDir = "./signing-keys" , pcTxFileDir = "./txs" diff --git a/examples/plutus-transfer/app/Main.hs b/examples/plutus-transfer/app/Main.hs index e385200e..a2f1d7e5 100644 --- a/examples/plutus-transfer/app/Main.hs +++ b/examples/plutus-transfer/app/Main.hs @@ -60,6 +60,7 @@ main = do , pcTipPollingInterval = 10_000_000 , pcSlotConfig = def , pcOwnPubKeyHash = "0f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546" + , pcOwnStakePubKeyHash = Nothing , pcScriptFileDir = "./scripts" , pcSigningKeyFileDir = "./signing-keys" , pcTxFileDir = "./txs" diff --git a/src/BotPlutusInterface/Balance.hs b/src/BotPlutusInterface/Balance.hs index 4ba1fe24..b96058ec 100644 --- a/src/BotPlutusInterface/Balance.hs +++ b/src/BotPlutusInterface/Balance.hs @@ -73,7 +73,7 @@ balanceTxIO :: balanceTxIO pabConf ownPkh unbalancedTx = runEitherT $ do - utxos <- newEitherT $ CardanoCLI.utxosAt @w pabConf $ Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing + utxos <- newEitherT $ CardanoCLI.utxosAt @w pabConf changeAddr privKeys <- newEitherT $ Files.readPrivateKeys @w pabConf let utxoIndex = fmap Tx.toTxOut utxos <> unBalancedTxUtxoIndex unbalancedTx requiredSigs = map Ledger.unPaymentPubKeyHash $ Map.keys (unBalancedTxRequiredSignatories unbalancedTx) @@ -101,17 +101,19 @@ balanceTxIO pabConf ownPkh unbalancedTx = -- If we have change but no change UTxO, we need to add an output for it -- We'll add a minimal output, run the loop again so it gets minUTxO, then update change balancedTxWithChange <- - if adaChange /= 0 && not (hasChangeUTxO ownPkh balancedTx) - then fst <$> loop utxoIndex privKeys minUtxos (addOutput ownPkh balancedTx) + if adaChange /= 0 && not (hasChangeUTxO changeAddr balancedTx) + then fst <$> loop utxoIndex privKeys minUtxos (addOutput changeAddr balancedTx) else pure balancedTx -- Get the updated change, add it to the tx let finalAdaChange = getAdaChange utxoIndex balancedTxWithChange - fullyBalancedTx = addAdaChange ownPkh finalAdaChange balancedTxWithChange + fullyBalancedTx = addAdaChange changeAddr finalAdaChange balancedTxWithChange -- finally, we must update the signatories hoistEither $ addSignatories ownPkh privKeys requiredSigs fullyBalancedTx where + changeAddr :: Address + changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) (pabConf.pcOwnStakePubKeyHash) loop :: Map TxOutRef TxOut -> Map PubKeyHash DummyPrivKey -> @@ -130,7 +132,7 @@ balanceTxIO pabConf ownPkh unbalancedTx = -- Calculate fees by pre-balancing the tx, building it, and running the CLI on result txWithoutFees <- - hoistEither $ balanceTxStep minUtxos utxoIndex ownPkh $ tx `withFee` 0 + hoistEither $ balanceTxStep minUtxos utxoIndex changeAddr $ tx `withFee` 0 exBudget <- newEitherT $ CardanoCLI.buildTx @w pabConf privKeys txWithoutFees nonBudgettedFees <- newEitherT $ CardanoCLI.calculateMinFee @w pabConf txWithoutFees @@ -140,7 +142,7 @@ balanceTxIO pabConf ownPkh unbalancedTx = lift $ printLog @w Debug $ "Fees: " ++ show fees -- Rebalance the initial tx with the above fees - balancedTx <- hoistEither $ balanceTxStep minUtxos utxoIndex ownPkh $ tx `withFee` fees + balancedTx <- hoistEither $ balanceTxStep minUtxos utxoIndex changeAddr $ tx `withFee` fees if balancedTx == tx then pure (balancedTx, minUtxos) @@ -175,13 +177,13 @@ calculateMinUtxos pabConf datums txOuts = balanceTxStep :: [(TxOut, Integer)] -> Map TxOutRef TxOut -> - PubKeyHash -> + Address -> Tx -> Either Text Tx -balanceTxStep minUtxos utxos ownPkh tx = +balanceTxStep minUtxos utxos changeAddr tx = Right (addLovelaces minUtxos tx) >>= balanceTxIns utxos - >>= handleNonAdaChange ownPkh utxos + >>= handleNonAdaChange changeAddr utxos -- | Get change value of a transaction, taking inputs, outputs, mint and fees into account getChange :: Map TxOutRef TxOut -> Tx -> Value @@ -288,10 +290,9 @@ addTxCollaterals utxos tx = do filterAdaOnly = Map.filter (isAdaOnly . txOutValue) -- | Ensures all non ada change goes back to user -handleNonAdaChange :: PubKeyHash -> Map TxOutRef TxOut -> Tx -> Either Text Tx -handleNonAdaChange ownPkh utxos tx = - let changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing - nonAdaChange = getNonAdaChange utxos tx +handleNonAdaChange :: Address -> Map TxOutRef TxOut -> Tx -> Either Text Tx +handleNonAdaChange changeAddr utxos tx = + let nonAdaChange = getNonAdaChange utxos tx outputs = case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of ([], txOuts) -> @@ -307,15 +308,13 @@ handleNonAdaChange ownPkh utxos tx = then Right $ if Value.isZero nonAdaChange then tx else tx {txOutputs = outputs} else Left "Not enough inputs to balance tokens." -hasChangeUTxO :: PubKeyHash -> Tx -> Bool -hasChangeUTxO ownPkh tx = +hasChangeUTxO :: Address -> Tx -> Bool +hasChangeUTxO changeAddr tx = any ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx - where - changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing -- | Adds ada change to a transaction, assuming there is already an output going to ownPkh. Otherwise, this is identity -addAdaChange :: PubKeyHash -> Integer -> Tx -> Tx -addAdaChange ownPkh change tx = +addAdaChange :: Address -> Integer -> Tx -> Tx +addAdaChange changeAddr change tx = tx { txOutputs = case partition ((==) changeAddr . Tx.txOutAddress) $ txOutputs tx of @@ -323,14 +322,11 @@ addAdaChange ownPkh change tx = txOut {txOutValue = v <> Ada.lovelaceValueOf change} : (txOuts <> txOuts') _ -> txOutputs tx } - where - changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing -- | Adds a 1 lovelace output to a transaction -addOutput :: PubKeyHash -> Tx -> Tx -addOutput ownPkh tx = tx {txOutputs = changeTxOut : txOutputs tx} +addOutput :: Address -> Tx -> Tx +addOutput changeAddr tx = tx {txOutputs = changeTxOut : txOutputs tx} where - changeAddr = Ledger.pubKeyHashAddress (Ledger.PaymentPubKeyHash ownPkh) Nothing changeTxOut = TxOut { txOutAddress = changeAddr @@ -338,7 +334,7 @@ addOutput ownPkh tx = tx {txOutputs = changeTxOut : txOutputs tx} , txOutDatumHash = Nothing } -{- | Add the required signatorioes to the transaction. Be aware the the signature itself is invalid, +{- | Add the required signatories to the transaction. Be aware the the signature itself is invalid, and will be ignored. Only the pub key hashes are used, mapped to signing key files on disk. -} addSignatories :: PubKeyHash -> Map PubKeyHash DummyPrivKey -> [PubKeyHash] -> Tx -> Either Text Tx diff --git a/src/BotPlutusInterface/CardanoCLI.hs b/src/BotPlutusInterface/CardanoCLI.hs index 01183126..19bd73d3 100644 --- a/src/BotPlutusInterface/CardanoCLI.hs +++ b/src/BotPlutusInterface/CardanoCLI.hs @@ -76,6 +76,7 @@ import Ledger.Tx ( TxInType (..), TxOut (..), TxOutRef (..), + txId, ) import Ledger.TxId (TxId (..)) import Ledger.Value (Value) @@ -176,7 +177,7 @@ calculateMinFee pabConf tx = , cmdArgs = mconcat [ ["transaction", "calculate-min-fee"] - , ["--tx-body-file", txFilePath pabConf "raw" tx] + , ["--tx-body-file", txFilePath pabConf "raw" (txId tx)] , ["--tx-in-count", showText $ length $ txInputs tx] , ["--tx-out-count", showText $ length $ txOutputs tx] , ["--witness-count", showText $ length $ txSignatures tx] @@ -295,7 +296,7 @@ buildTx pabConf privKeys tx = do , ["--fee", showText . getLovelace . fromValue $ txFee tx] , mconcat [ ["--protocol-params-file", pabConf.pcProtocolParamsFile] - , ["--out-file", txFilePath pabConf "raw" tx] + , ["--out-file", txFilePath pabConf "raw" (txId tx)] ] ] @@ -318,9 +319,9 @@ signTx pabConf tx pubKeys = opts = mconcat [ ["transaction", "sign"] - , ["--tx-body-file", txFilePath pabConf "raw" tx] + , ["--tx-body-file", txFilePath pabConf "raw" (txId tx)] , signingKeyFiles - , ["--out-file", txFilePath pabConf "signed" tx] + , ["--out-file", txFilePath pabConf "signed" (txId tx)] ] budgetFromConfig :: PABConfig -> ExBudget -> ExBudget @@ -343,7 +344,7 @@ submitTx pabConf tx = "cardano-cli" ( mconcat [ ["transaction", "submit"] - , ["--tx-file", txFilePath pabConf "signed" tx] + , ["--tx-file", txFilePath pabConf "signed" (txId tx)] , networkOpt pabConf ] ) @@ -480,8 +481,8 @@ networkOpt pabConf = case pabConf.pcNetwork of Mainnet -> ["--mainnet"] txOutRefToCliArg :: TxOutRef -> Text -txOutRefToCliArg (TxOutRef (TxId txId) txIx) = - encodeByteString (fromBuiltin txId) <> "#" <> showText txIx +txOutRefToCliArg (TxOutRef (TxId tId) txIx) = + encodeByteString (fromBuiltin tId) <> "#" <> showText txIx flatValueToCliArg :: (CurrencySymbol, TokenName, Integer) -> Text flatValueToCliArg (curSymbol, name, amount) diff --git a/src/BotPlutusInterface/Contract.hs b/src/BotPlutusInterface/Contract.hs index 4c2fdb24..246fabfa 100644 --- a/src/BotPlutusInterface/Contract.hs +++ b/src/BotPlutusInterface/Contract.hs @@ -7,28 +7,33 @@ import BotPlutusInterface.Balance qualified as PreBalance import BotPlutusInterface.CardanoCLI qualified as CardanoCLI import BotPlutusInterface.Effects ( PABEffect, + ShellArgs (..), + callCommand, createDirectoryIfMissing, handlePABEffect, logToContract, printLog, queryChainIndex, + readFileTextEnvelope, threadDelay, uploadDir, ) import BotPlutusInterface.Files (DummyPrivKey (FromSKey, FromVKey)) import BotPlutusInterface.Files qualified as Files -import BotPlutusInterface.Types (ContractEnvironment (..), LogLevel (Debug, Warn), Tip (slot)) -import Control.Lens ((^.)) -import Control.Monad (void) +import BotPlutusInterface.Types (ContractEnvironment (..), LogLevel (Debug, Warn), Tip (block, slot)) +import Cardano.Api (AsType (..), EraInMode (..), Tx (Tx)) +import Control.Lens (preview, (^.)) +import Control.Monad (join, void, when) import Control.Monad.Freer (Eff, Member, interpret, reinterpret, runM, subsume, type (~>)) import Control.Monad.Freer.Error (runError) import Control.Monad.Freer.Extras.Log (handleLogIgnore) import Control.Monad.Freer.Extras.Modify (raiseEnd) import Control.Monad.Freer.Writer (Writer (Tell)) import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.Either (eitherT, firstEitherT, newEitherT, secondEitherT) +import Control.Monad.Trans.Either (EitherT, eitherT, firstEitherT, newEitherT) import Data.Aeson (ToJSON, Value) import Data.Aeson.Extras (encodeByteString) +import Data.Either (fromRight) import Data.Kind (Type) import Data.Map qualified as Map import Data.Row (Row) @@ -42,13 +47,16 @@ import Ledger.Slot (Slot (Slot)) import Ledger.TimeSlot (posixTimeRangeToContainedSlotRange, posixTimeToEnclosingSlot, slotToEndPOSIXTime) import Ledger.Tx (CardanoTx) import Ledger.Tx qualified as Tx -import Plutus.ChainIndex.Types (RollbackState (Committed), TxValidity (..)) +import Plutus.ChainIndex.TxIdState (fromTx, transactionStatus) +import Plutus.ChainIndex.Types (RollbackState (..), TxIdState, TxStatus) import Plutus.Contract.Checkpoint (Checkpoint (..)) import Plutus.Contract.Effects ( BalanceTxResponse (..), + ChainIndexQuery (..), PABReq (..), PABResp (..), WriteBalancedTxResponse (..), + _TxIdResponse, ) import Plutus.Contract.Resumable (Resumable (..)) import Plutus.Contract.Types (Contract (..), ContractEffs) @@ -148,13 +156,13 @@ handlePABReq contractEnv req = do PosixTimeRangeToContainedSlotRangeResp $ Right $ posixTimeRangeToContainedSlotRange contractEnv.cePABConfig.pcSlotConfig posixTimeRange + AwaitTxStatusChangeReq txId -> AwaitTxStatusChangeResp txId <$> awaitTxStatusChange @w contractEnv txId ------------------------ -- Unhandled requests -- ------------------------ -- AwaitTimeReq t -> pure $ AwaitTimeResp t -- AwaitUtxoSpentReq txOutRef -> pure $ AwaitUtxoSpentResp ChainIndexTx -- AwaitUtxoProducedReq Address -> pure $ AwaitUtxoProducedResp (NonEmpty ChainIndexTx) - AwaitTxStatusChangeReq txId -> pure $ AwaitTxStatusChangeResp txId (Committed TxValid ()) -- AwaitTxOutStatusChangeReq TxOutRef -- ExposeEndpointReq ActiveEndpoint -> ExposeEndpointResp EndpointDescription (EndpointValue JSON.Value) -- YieldUnbalancedTxReq UnbalancedTx @@ -163,6 +171,42 @@ handlePABReq contractEnv req = do printLog @w Debug $ show resp pure resp +awaitTxStatusChange :: + forall (w :: Type) (effs :: [Type -> Type]). + Member (PABEffect w) effs => + ContractEnvironment w -> + Ledger.TxId -> + Eff effs TxStatus +awaitTxStatusChange contractEnv txId = do + -- The depth (in blocks) after which a transaction cannot be rolled back anymore (from Plutus.ChainIndex.TxIdState) + let chainConstant = 8 + + mTx <- queryChainIndexForTxState + case mTx of + Nothing -> pure Unknown + Just txState -> do + awaitNBlocks @w contractEnv (chainConstant + 1) + -- Check if the tx is still present in chain-index, in case of a rollback + -- we might not find it anymore. + ciTxState' <- queryChainIndexForTxState + case ciTxState' of + Nothing -> pure Unknown + Just _ -> do + blk <- fromInteger <$> currentBlock contractEnv + -- This will set the validity correctly based on the txState. + -- The tx will always be committed, as we wait for chainConstant + 1 blocks + let status = transactionStatus blk txState txId + pure $ fromRight Unknown status + where + queryChainIndexForTxState :: Eff effs (Maybe TxIdState) + queryChainIndexForTxState = do + mTx <- join . preview _TxIdResponse <$> (queryChainIndex @w $ TxFromTxId txId) + case mTx of + Just tx -> do + blk <- fromInteger <$> currentBlock contractEnv + pure . Just $ fromTx blk tx + Nothing -> pure Nothing + -- | This will FULLY balance a transaction balanceTx :: forall (w :: Type) (effs :: [Type -> Type]). @@ -194,7 +238,7 @@ writeBalancedTx contractEnv (Right tx) = do uploadDir @w pabConf.pcSigningKeyFileDir createDirectoryIfMissing @w False (Text.unpack pabConf.pcScriptFileDir) - eitherT (pure . WriteBalancedTxFailed . OtherError) (pure . WriteBalancedTxSuccess . Right) $ do + eitherT (pure . WriteBalancedTxFailed . OtherError) (pure . WriteBalancedTxSuccess . Left) $ do void $ firstEitherT (Text.pack . show) $ newEitherT $ Files.writeAll @w pabConf tx lift $ uploadDir @w pabConf.pcScriptFileDir @@ -206,18 +250,39 @@ writeBalancedTx contractEnv (Right tx) = do void $ newEitherT $ CardanoCLI.buildTx @w pabConf privKeys tx + -- TODO: This whole part is hacky and we should remove it. + let path = Text.unpack $ Files.txFilePath pabConf "raw" (Tx.txId tx) + -- We read back the tx from file as tx currently has the wrong id (but the one we create with cardano-cli is correct) + alonzoBody <- firstEitherT (Text.pack . show) $ newEitherT $ readFileTextEnvelope @w (AsTxBody AsAlonzoEra) path + let cardanoTx = Tx.SomeTx (Tx alonzoBody []) AlonzoEraInCardanoMode + if signable then newEitherT $ CardanoCLI.signTx @w pabConf tx requiredSigners else lift . printLog @w Warn . Text.unpack . Text.unlines $ [ "Not all required signatures have signing key files. Please sign and submit the tx manually:" - , "Tx file: " <> Files.txFilePath pabConf "raw" tx + , "Tx file: " <> Files.txFilePath pabConf "raw" (Tx.txId tx) , "Signatories (pkh): " <> Text.unwords (map pkhToText requiredSigners) ] - if not pabConf.pcDryRun && signable - then secondEitherT (const tx) $ newEitherT $ CardanoCLI.submitTx @w pabConf tx - else pure tx + when (not pabConf.pcDryRun && signable) $ do + newEitherT $ CardanoCLI.submitTx @w pabConf tx + + -- We need to replace the outfile we created at the previous step, as it currently still has the old (incorrect) id + mvFiles (Files.txFilePath pabConf "raw" (Tx.txId tx)) (Files.txFilePath pabConf "raw" (Ledger.getCardanoTxId $ Left cardanoTx)) + when signable $ mvFiles (Files.txFilePath pabConf "signed" (Tx.txId tx)) (Files.txFilePath pabConf "signed" (Ledger.getCardanoTxId $ Left cardanoTx)) + + pure cardanoTx + where + mvFiles :: Text -> Text -> EitherT Text (Eff effs) () + mvFiles src dst = + newEitherT $ + callCommand @w + ShellArgs + { cmdName = "mv" + , cmdArgs = [src, dst] + , cmdOutParser = const () + } pkhToText :: Ledger.PubKey -> Text pkhToText = encodeByteString . fromBuiltin . Ledger.getPubKeyHash . Ledger.pubKeyHash @@ -239,6 +304,26 @@ awaitSlot contractEnv s@(Slot n) = do | n < tip'.slot -> pure $ Slot tip'.slot _ -> awaitSlot contractEnv s +-- | Wait for n Blocks. +awaitNBlocks :: + forall (w :: Type) (effs :: [Type -> Type]). + Member (PABEffect w) effs => + ContractEnvironment w -> + Integer -> + Eff effs () +awaitNBlocks contractEnv n = do + current <- currentBlock contractEnv + go current + where + go :: Integer -> Eff effs () + go start = do + threadDelay @w (fromIntegral contractEnv.cePABConfig.pcTipPollingInterval) + tip <- CardanoCLI.queryTip @w contractEnv.cePABConfig + case tip of + Right tip' + | start + n <= tip'.block -> pure () + _ -> go start + {- | Wait at least until the given time. Uses the awaitSlot under the hood, so the same constraints are applying here as well. -} @@ -261,6 +346,14 @@ currentSlot :: currentSlot contractEnv = Slot . slot . either (error . Text.unpack) id <$> CardanoCLI.queryTip @w contractEnv.cePABConfig +currentBlock :: + forall (w :: Type) (effs :: [Type -> Type]). + Member (PABEffect w) effs => + ContractEnvironment w -> + Eff effs Integer +currentBlock contractEnv = + block . either (error . Text.unpack) id <$> CardanoCLI.queryTip @w contractEnv.cePABConfig + currentTime :: forall (w :: Type) (effs :: [Type -> Type]). Member (PABEffect w) effs => diff --git a/src/BotPlutusInterface/Files.hs b/src/BotPlutusInterface/Files.hs index 8f5e0bd2..7bd5bea2 100644 --- a/src/BotPlutusInterface/Files.hs +++ b/src/BotPlutusInterface/Files.hs @@ -115,8 +115,8 @@ signingKeyFilePath pabConf (PubKeyHash pubKeyHash) = let h = encodeByteString $ fromBuiltin pubKeyHash in pabConf.pcSigningKeyFileDir <> "/signing-key-" <> h <> ".skey" -txFilePath :: PABConfig -> Text -> Tx.Tx -> Text -txFilePath pabConf ext tx = pabConf.pcTxFileDir <> "/" <> txFileName (Tx.txId tx) ext +txFilePath :: PABConfig -> Text -> TxId.TxId -> Text +txFilePath pabConf ext txId = pabConf.pcTxFileDir <> "/" <> txFileName txId ext txFileName :: TxId.TxId -> Text -> Text txFileName txId ext = "tx-" <> txIdToText txId <> "." <> ext diff --git a/src/BotPlutusInterface/Helpers.hs b/src/BotPlutusInterface/Helpers.hs new file mode 100644 index 00000000..f0f97909 --- /dev/null +++ b/src/BotPlutusInterface/Helpers.hs @@ -0,0 +1,29 @@ +module BotPlutusInterface.Helpers (awaitTxConfirmedUntilSlot) where + +import Control.Lens (review) +import Data.Text (pack) +import Ledger (Slot, TxId) +import Plutus.Contract.Error (AsContractError, _OtherContractError) +import Plutus.Contract.Request (RollbackState (Unknown), awaitTxStatusChange, currentSlot, waitNSlots) +import Plutus.Contract.Types (Contract, throwError) +import Prelude + +awaitTxConfirmedUntilSlot :: forall w s e. (AsContractError e) => TxId -> Slot -> Contract w s e () +awaitTxConfirmedUntilSlot txId maxSlot = go 0 + where + go :: Integer -> Contract w s e () + go n = do + mTx <- awaitTxStatusChange txId + case mTx of + Unknown -> do + curSlot <- currentSlot + if curSlot > maxSlot + then + throwError @e $ + review _OtherContractError $ + pack $ + "Could not find transaction - " ++ show txId ++ " - before slot " ++ show maxSlot + else do + _ <- waitNSlots 20 + go (n + 1) + _ -> pure () diff --git a/src/BotPlutusInterface/Types.hs b/src/BotPlutusInterface/Types.hs index 08a7f0c8..fc6b5eff 100644 --- a/src/BotPlutusInterface/Types.hs +++ b/src/BotPlutusInterface/Types.hs @@ -28,7 +28,7 @@ import Data.Kind (Type) import Data.Map (Map) import Data.Text (Text) import GHC.Generics (Generic) -import Ledger (PubKeyHash) +import Ledger (PubKeyHash, StakePubKeyHash) import Ledger.TimeSlot (SlotConfig) import Network.Wai.Handler.Warp (Port) import Numeric.Natural (Natural) @@ -62,6 +62,7 @@ data PABConfig = PABConfig pcDryRun :: !Bool , pcLogLevel :: !LogLevel , pcOwnPubKeyHash :: !PubKeyHash + , pcOwnStakePubKeyHash :: !(Maybe StakePubKeyHash) , pcTipPollingInterval :: !Natural , -- | Forced budget for scripts, as optional (CPU Steps, Memory Units) pcForceBudget :: !(Maybe (Integer, Integer)) @@ -127,6 +128,7 @@ instance Default PABConfig where , pcProtocolParamsFile = "./protocol.json" , pcLogLevel = Info , pcOwnPubKeyHash = "" + , pcOwnStakePubKeyHash = Nothing , pcForceBudget = Nothing , pcPort = 9080 , pcEnableTxEndpoint = False diff --git a/test/Spec/BotPlutusInterface/Balance.hs b/test/Spec/BotPlutusInterface/Balance.hs index e849589f..c38cebf1 100644 --- a/test/Spec/BotPlutusInterface/Balance.hs +++ b/test/Spec/BotPlutusInterface/Balance.hs @@ -61,9 +61,9 @@ addUtxosForFees = do tx = mempty {txOutputs = [txout]} `withFee` 500_000 minUtxo = [(txout, 1_000_000)] utxoIndex = Map.fromList [utxo1, utxo2, utxo3] - ownPkh = pkh1 + ownAddr = addr1 balancedTx = - Balance.balanceTxStep minUtxo utxoIndex ownPkh tx + Balance.balanceTxStep minUtxo utxoIndex ownAddr tx txInputs <$> balancedTx @?= Right (Set.fromList [txIn1, txIn2]) @@ -73,9 +73,9 @@ addUtxosForNativeTokens = do tx = mempty {txOutputs = [txout]} `withFee` 500_000 minUtxo = [(txout, 1_000_000)] utxoIndex = Map.fromList [utxo1, utxo2, utxo3, utxo4] - ownPkh = pkh1 + ownAddr = addr1 balancedTx = - Balance.balanceTxStep minUtxo utxoIndex ownPkh tx + Balance.balanceTxStep minUtxo utxoIndex ownAddr tx txInputs <$> balancedTx @?= Right (Set.fromList [txIn1, txIn2, txIn3, txIn4]) @@ -85,8 +85,8 @@ addUtxosForChange = do tx = mempty {txOutputs = [txout]} `withFee` 500_000 minUtxo = [(txout, 1_000_000)] utxoIndex = Map.fromList [utxo1, utxo2, utxo3] - ownPkh = pkh1 + ownAddr = addr1 balancedTx = - Balance.balanceTxStep minUtxo utxoIndex ownPkh tx + Balance.balanceTxStep minUtxo utxoIndex ownAddr tx txInputs <$> balancedTx @?= Right (Set.fromList [txIn1, txIn2]) diff --git a/test/Spec/BotPlutusInterface/Contract.hs b/test/Spec/BotPlutusInterface/Contract.hs index f1bce84d..b1483a55 100644 --- a/test/Spec/BotPlutusInterface/Contract.hs +++ b/test/Spec/BotPlutusInterface/Contract.hs @@ -12,11 +12,11 @@ import Data.Char (isSpace) import Data.Default (def) import Data.Function (on) import Data.Kind (Type) +import Data.List qualified as List import Data.Map qualified as Map import Data.Maybe (fromMaybe) import Data.Monoid (Last (Last)) import Data.Row (Row) -import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as Text import Data.Void (Void) @@ -108,7 +108,7 @@ sendAda = do Constraints.mustPayToPubKey paymentPkh2 (Ada.lovelaceValueOf 1000) submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -162,16 +162,16 @@ sendAda = do --tx-out ${addr2}+1000 --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 300 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) , ( 13 , [text| cardano-cli transaction sign - --tx-body-file ./txs/tx-${outTxId}.raw + --tx-body-file ./txs/tx-?.raw --signing-key-file ./signing-keys/signing-key-${pkh1'}.skey - --out-file ./txs/tx-${outTxId}.signed + --out-file ./txs/tx-?.signed |] ) ] @@ -189,7 +189,7 @@ sendAdaNoChange = do Constraints.mustPayToPubKey paymentPkh2 (Ada.lovelaceValueOf 1000) submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -201,7 +201,7 @@ sendAdaNoChange = do --tx-out ${addr2}+1000 --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 200 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -222,7 +222,7 @@ sendAdaStaking = do Constraints.mustPayToPubKeyAddress paymentPkh2 stakePkh3 (Ada.lovelaceValueOf 1000) submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -274,16 +274,16 @@ sendAdaStaking = do --tx-out ${addr2Staking}+1000 --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 200 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) , ( 7 , [text| cardano-cli transaction sign - --tx-body-file ./txs/tx-${outTxId}.raw + --tx-body-file ./txs/tx-?.raw --signing-key-file ./signing-keys/signing-key-${pkh1'}.skey - --out-file ./txs/tx-${outTxId}.signed + --out-file ./txs/tx-?.signed |] ) ] @@ -303,7 +303,7 @@ multisigSupport = do submitTx constraints -- Building and siging the tx should include both signing keys - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -328,17 +328,17 @@ multisigSupport = do --required-signer ./signing-keys/signing-key-${pkh1'}.skey --required-signer ./signing-keys/signing-key-${pkh3'}.skey --fee 200 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) , ( 7 , [text| cardano-cli transaction sign - --tx-body-file ./txs/tx-${outTxId}.raw + --tx-body-file ./txs/tx-?.raw --signing-key-file ./signing-keys/signing-key-${pkh1'}.skey --signing-key-file ./signing-keys/signing-key-${pkh3'}.skey - --out-file ./txs/tx-${outTxId}.signed + --out-file ./txs/tx-?.signed |] ) ] @@ -366,7 +366,7 @@ withoutSigning = do submitTx constraints -- Building and siging the tx should include both signing keys - assertContractWithTxId contract initState $ \state outTxId -> do + assertContract contract initState $ \state -> do assertCommandHistory state [ @@ -379,7 +379,7 @@ withoutSigning = do --required-signer ./signing-keys/signing-key-${pkh1'}.skey --required-signer-hash ${pkh3'} --fee 200 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -411,7 +411,7 @@ sendTokens = do (Ada.lovelaceValueOf 1000 <> Value.singleton "abcd1234" "testToken" 5) submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -424,7 +424,7 @@ sendTokens = do --tx-out ${addr2}+1000 + 5 abcd1234.74657374546F6B656E --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 300 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -455,7 +455,7 @@ sendTokensWithoutName = do (Ada.lovelaceValueOf 1000 <> Value.singleton "abcd1234" "" 5) submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -468,7 +468,7 @@ sendTokensWithoutName = do --tx-out ${addr2}+1000 + 5 abcd1234 --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 300 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -506,7 +506,7 @@ mintTokens = do (Ada.lovelaceValueOf 1000 <> Value.singleton curSymbol "testToken" 5) submitTxConstraintsWith @Void lookups constraints - assertContractWithTxId contract initState $ \state outTxId -> do + assertContract contract initState $ \state -> do assertCommandHistory state [ @@ -539,7 +539,7 @@ mintTokens = do --mint 5 ${curSymbol'}.74657374546F6B656E --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 502300 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -549,8 +549,8 @@ mintTokens = do [ [text|./result-scripts/policy-${curSymbol'}.plutus|] , [text|./result-scripts/redeemer-${redeemerHash}.json|] , [text|./signing-keys/signing-key-${pkh1'}.skey|] - , [text|./txs/tx-${outTxId}.raw|] - , [text|./txs/tx-${outTxId}.signed|] + , [text|./txs/tx-?.raw|] + , [text|./txs/tx-?.signed|] ] spendToValidator :: Assertion @@ -600,7 +600,7 @@ spendToValidator = do Constraints.mustPayToOtherScript valHash datum (Ada.lovelaceValueOf 500) submitTxConstraintsWith @Void lookups constraints - assertContractWithTxId contract initState $ \state outTxId -> do + assertContract contract initState $ \state -> do assertCommandHistory state [ @@ -627,7 +627,7 @@ spendToValidator = do --tx-out-datum-embed-file ./result-scripts/datum-${datumHash'}.json --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 300 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -636,8 +636,8 @@ spendToValidator = do state [ [text|./result-scripts/datum-${datumHash'}.json|] , [text|./signing-keys/signing-key-${pkh1'}.skey|] - , [text|./txs/tx-${outTxId}.raw|] - , [text|./txs/tx-${outTxId}.signed|] + , [text|./txs/tx-?.raw|] + , [text|./txs/tx-?.signed|] ] redeemFromValidator :: Assertion @@ -691,7 +691,7 @@ redeemFromValidator = do <> Constraints.mustPayToPubKey paymentPkh2 (Ada.lovelaceValueOf 500) submitTxConstraintsWith @Void lookups constraints - assertContractWithTxId contract initState $ \state outTxId -> do + assertContract contract initState $ \state -> do assertCommandHistory state [ @@ -724,7 +724,7 @@ redeemFromValidator = do --tx-out ${addr2}+500 --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 502400 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -735,8 +735,8 @@ redeemFromValidator = do , [text|./result-scripts/redeemer-${redeemerHash}.json|] , [text|./result-scripts/validator-${valHash'}.plutus|] , [text|./signing-keys/signing-key-${pkh1'}.skey|] - , [text|./txs/tx-${outTxId}.raw|] - , [text|./txs/tx-${outTxId}.signed|] + , [text|./txs/tx-?.raw|] + , [text|./txs/tx-?.signed|] ] multiTx :: Assertion @@ -758,18 +758,15 @@ multiTx = do case result of Left errMsg -> assertFailure (show errMsg) - Right [tx1, tx2] -> - let outTxId1 = encodeByteString $ fromBuiltin $ TxId.getTxId $ Tx.getCardanoTxId tx1 - outTxId2 = encodeByteString $ fromBuiltin $ TxId.getTxId $ Tx.getCardanoTxId tx2 - in assertFiles - state - [ [text|./signing-keys/signing-key-${pkh1'}.skey|] - , [text|./txs/tx-${outTxId1}.raw|] - , [text|./txs/tx-${outTxId2}.raw|] - , [text|./txs/tx-${outTxId1}.signed|] - , [text|./txs/tx-${outTxId2}.signed|] - ] - Right _ -> assertFailure "Wrong number of txs" + Right _ -> + assertFiles + state + [ [text|./signing-keys/signing-key-${pkh1'}.skey|] + , [text|./txs/tx-?.raw|] + , [text|./txs/tx-?.raw|] + , [text|./txs/tx-?.signed|] + , [text|./txs/tx-?.signed|] + ] withValidRange :: Assertion withValidRange = do @@ -785,7 +782,7 @@ withValidRange = do <> Constraints.mustValidateIn (interval (POSIXTime 1643636293000) (POSIXTime 1646314693000)) submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> + assertContract contract initState $ \state -> assertCommandHistory state [ @@ -813,7 +810,7 @@ withValidRange = do --invalid-hereafter 50255602 --required-signer ./signing-keys/signing-key-${pkh1'}.skey --fee 200 - --protocol-params-file ./protocol.json --out-file ./txs/tx-${outTxId}.raw + --protocol-params-file ./protocol.json --out-file ./txs/tx-?.raw |] ) ] @@ -829,13 +826,11 @@ useWriter = do tell $ Last $ Just "Init contract" let constraints = Constraints.mustPayToPubKey paymentPkh2 (Ada.lovelaceValueOf 1000) - txId <- submitTx constraints - tell $ Last $ Just $ Text.pack $ show $ Tx.txId <$> txId - pure txId + submitTx constraints - assertContractWithTxId contract initState $ \state outTxId -> do + assertContract contract initState $ \state -> do (state ^. observableState) - @?= Last (Just ("Right " <> outTxId)) + @?= Last (Just "Init contract") waitNextBlock :: Assertion waitNextBlock = do @@ -863,8 +858,9 @@ waitNextBlock = do assertFiles :: forall (w :: Type). MockContractState w -> [Text] -> Assertion assertFiles state expectedFiles = assertBool errorMsg $ - Set.fromList (map Text.unpack expectedFiles) `Set.isSubsetOf` Map.keysSet (state ^. files) + List.null $ List.deleteFirstsBy (flip commandEqual) expectedFiles fileNames where + fileNames = Text.pack <$> Map.keys (state ^. files) errorMsg = unlines [ "expected (at least):" @@ -889,6 +885,20 @@ assertContractWithTxId contract initState assertion = do let outTxId = encodeByteString $ fromBuiltin $ TxId.getTxId $ Tx.getCardanoTxId tx in assertion state outTxId +assertContract :: + forall (w :: Type) (s :: Row Type). + (ToJSON w, Monoid w) => + Contract w s Text CardanoTx -> + MockContractState w -> + (MockContractState w -> Assertion) -> + Assertion +assertContract contract initState assertion = do + let (result, state) = runContractPure contract initState + + case result of + Left errMsg -> assertFailure (show errMsg) + Right _ -> assertion state + assertCommandHistory :: forall (w :: Type). MockContractState w -> [(Int, Text)] -> Assertion assertCommandHistory state = mapM_ diff --git a/test/Spec/MockContract.hs b/test/Spec/MockContract.hs index 7ab0e3e0..b99050d1 100644 --- a/test/Spec/MockContract.hs +++ b/test/Spec/MockContract.hs @@ -63,7 +63,7 @@ import Cardano.Api ( NetworkId (Mainnet), PaymentKey, SigningKey (PaymentSigningKey), - TextEnvelope, + TextEnvelope (TextEnvelope, teDescription, teRawCBOR, teType), TextEnvelopeDescr, TextEnvelopeError (TextEnvelopeAesonDecodeError), deserialiseFromTextEnvelope, @@ -86,8 +86,8 @@ import Data.Aeson qualified as JSON import Data.Aeson.Extras (encodeByteString) import Data.ByteString qualified as ByteString import Data.Default (Default (def)) -import Data.Either.Combinators (mapLeft) -import Data.Hex (hex) +import Data.Either.Combinators (fromRight, mapLeft) +import Data.Hex (hex, unhex) import Data.Kind (Type) import Data.List (isPrefixOf, sortOn) import Data.Map (Map) @@ -326,26 +326,27 @@ mockCallCommand ShellArgs {cmdName, cmdArgs, cmdOutParser} = do ("cardano-cli", "transaction" : "build-raw" : args) -> do case drop 1 $ dropWhile (/= "--out-file") args of filepath : _ -> - modify @(MockContractState w) (files . at (Text.unpack filepath) ?~ OtherFile "TxBody") + modify @(MockContractState w) (files . at (Text.unpack filepath) ?~ TextEnvelopeFile dummyTxRawFile) _ -> throwError @Text "Out file argument is missing" pure $ Right $ cmdOutParser "" ("cardano-cli", "transaction" : "build" : args) -> do case drop 1 $ dropWhile (/= "--out-file") args of filepath : _ -> - modify @(MockContractState w) (files . at (Text.unpack filepath) ?~ OtherFile "TxBody") + modify @(MockContractState w) (files . at (Text.unpack filepath) ?~ TextEnvelopeFile dummyTxRawFile) _ -> throwError @Text "Out file argument is missing" pure $ Right $ cmdOutParser "" ("cardano-cli", "transaction" : "sign" : args) -> do case drop 1 $ dropWhile (/= "--out-file") args of filepath : _ -> - modify @(MockContractState w) (files . at (Text.unpack filepath) ?~ OtherFile "Tx") + modify @(MockContractState w) (files . at (Text.unpack filepath) ?~ TextEnvelopeFile dummyTxSignedFile) _ -> throwError @Text "Out file argument is missing" pure $ Right $ cmdOutParser "" ("cardano-cli", "transaction" : "submit" : _) -> pure $ Right $ cmdOutParser "" + ("mv", _) -> pure $ Right $ cmdOutParser "" (unsupportedCmd, unsupportedArgs) -> throwError @Text ("Unsupported command: " <> Text.intercalate " " (unsupportedCmd : unsupportedArgs)) @@ -514,9 +515,24 @@ mockQueryChainIndex = \case TxOutFromRef txOutRef -> do state <- get @(MockContractState w) pure $ TxOutRefResponse $ Tx.fromTxOut =<< lookup txOutRef (state ^. utxos) - TxFromTxId _ -> - -- pure $ TxIdResponse Nothing - throwError @Text "TxFromTxId is unimplemented" + TxFromTxId txId -> do + -- TODO: Track some kind of state here, add tests to ensure this works correctly + -- For now, empty txs + state <- get @(MockContractState w) + let knownUtxos = state ^. utxos + pure $ + TxIdResponse $ + Just $ + ChainIndexTx + { _citxTxId = txId + , _citxInputs = mempty + , _citxOutputs = buildOutputsFromKnownUTxOs knownUtxos txId + , _citxValidRange = Ledger.always + , _citxData = mempty + , _citxRedeemers = mempty + , _citxScripts = mempty + , _citxCardanoTx = Nothing + } UtxoSetMembership _ -> throwError @Text "UtxoSetMembership is unimplemented" UtxoSetAtAddress pageQuery _ -> do @@ -565,3 +581,19 @@ buildOutputsFromKnownUTxOs knownUtxos txId = ValidTx $ fillGaps sortedRelatedRef | n' == n = txOut : fillGaps outs (n + 1) | otherwise = defTxOut : fillGaps (out : outs) (n + 1) defTxOut = TxOut (Ledger.Address (PubKeyCredential "") Nothing) mempty Nothing + +dummyTxRawFile :: TextEnvelope +dummyTxRawFile = + TextEnvelope + { teType = "TxBodyAlonzo" + , teDescription = "" + , teRawCBOR = fromRight (error "failed to unpack CBOR hex") $ unhex "86a500848258205d677265fa5bb21ce6d8c7502aca70b9316d10e958611f3c6b758f65ad9599960182582076ed2fcda860de2cbacd0f3a169058fa91eff47bc1e1e5b6d84497159fbc9300008258209405c89393ba84b14bf8d3e7ed4788cc6e2257831943b58338bee8d37a3668fc00825820a1be9565ccac4a04d2b5bf0d0167196ae467da0d88161c9c827fbe76452b24ef000d8182582076ed2fcda860de2cbacd0f3a169058fa91eff47bc1e1e5b6d84497159fbc930000018482581d600f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f975461a3b8cc4a582581d600f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546821a00150bd0a1581c1d6445ddeda578117f393848e685128f1e78ad0c4e48129c5964dc2ea14974657374546f6b656e1a000d062782581d606696936bb8ae24859d0c2e4d05584106601f58a5e9466282c8561b88821a00150bd0a1581c1d6445ddeda578117f393848e685128f1e78ad0c4e48129c5964dc2ea14974657374546f6b656e1282581d60981fc565bcf0c95c0cfa6ee6693875b60d529d87ed7082e9bf03c6a4821a00150bd0a1581c1d6445ddeda578117f393848e685128f1e78ad0c4e48129c5964dc2ea14974657374546f6b656e0f021a000320250e81581c0f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f975469fff8080f5f6" + } + +dummyTxSignedFile :: TextEnvelope +dummyTxSignedFile = + TextEnvelope + { teType = "Tx AlonzoEra" + , teDescription = "" + , teRawCBOR = fromRight (error "failed to unpack CBOR hex") $ unhex "84a500848258205d677265fa5bb21ce6d8c7502aca70b9316d10e958611f3c6b758f65ad9599960182582076ed2fcda860de2cbacd0f3a169058fa91eff47bc1e1e5b6d84497159fbc9300008258209405c89393ba84b14bf8d3e7ed4788cc6e2257831943b58338bee8d37a3668fc00825820a1be9565ccac4a04d2b5bf0d0167196ae467da0d88161c9c827fbe76452b24ef000d8182582076ed2fcda860de2cbacd0f3a169058fa91eff47bc1e1e5b6d84497159fbc930000018482581d600f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f975461a3b8cc4a582581d600f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546821a00150bd0a1581c1d6445ddeda578117f393848e685128f1e78ad0c4e48129c5964dc2ea14974657374546f6b656e1a000d062782581d606696936bb8ae24859d0c2e4d05584106601f58a5e9466282c8561b88821a00150bd0a1581c1d6445ddeda578117f393848e685128f1e78ad0c4e48129c5964dc2ea14974657374546f6b656e1282581d60981fc565bcf0c95c0cfa6ee6693875b60d529d87ed7082e9bf03c6a4821a00150bd0a1581c1d6445ddeda578117f393848e685128f1e78ad0c4e48129c5964dc2ea14974657374546f6b656e0f021a000320250e81581c0f45aaf1b2959db6e5ff94dbb1f823bf257680c3c723ac2d49f97546a10081825820096092b8515d75c2a2f75d6aa7c5191996755840e81deaa403dba5b690f091b65840295a93849a67cecabb8286e561c407b6bd49abf8d2da8bfb821105eae4d28ef0ef1b9ee5e8abb8fd334059f3dfc78c0a65e74057a2dc8d1d12e46842abea600ff5f6" + }