@laceio/sdk 0.1.1 → 0.1.2-canary.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{TransactionBuilder-DrB3NVTi.cjs → TransactionBuilder-X1H9Gze6.cjs} +64 -14
- package/dist/{TransactionBuilder-V6aLx4SV.js → TransactionBuilder-bp2aMDhg.js} +23 -15
- package/dist/{_polyfill-buffer-patch-BnPVerlG.cjs → _polyfill-buffer-patch-CM8X7JiY.cjs} +21 -2
- package/dist/_polyfill-buffer-patch-yNxN4d5E.js +1467 -0
- package/dist/_web-crypto-random-bytes-Bo-GTcRS.js +197 -0
- package/dist/acc-module-4iIUv6My.cjs +27654 -0
- package/dist/acc-module-Cmj8EsIb.js +27624 -0
- package/dist/account-BOQruszJ.js +6 -0
- package/dist/account-BUOj4pYJ.cjs +11 -0
- package/dist/account-call-7wnopBq0.cjs +5 -0
- package/dist/account-call-B9d_2Xwi.js +4 -0
- package/dist/account-call-Bd4Rpbvt.cjs +323 -0
- package/dist/account-call-C3CkDqEC.js +276 -0
- package/dist/account-guards-BtjTGxDZ.cjs +42 -0
- package/dist/account-guards-D9Lu5T3W.js +36 -0
- package/dist/account-tokens-DvkOFVON.js +16 -0
- package/dist/account-tokens-nwcvwLu4.cjs +21 -0
- package/dist/{activity-type.stable-BJEfeLxL.cjs → activity-type-Ch6ljxBy.cjs} +2 -1
- package/dist/{activity-type.stable-C76_hWd7.js → activity-type-GsJGWs-3.js} +2 -1
- package/dist/address-B9ntKL8K.js +13761 -0
- package/dist/address-BXp-mY8H.cjs +11270 -0
- package/dist/{address-book-address-validator-DIUolI-s.js → address-book-address-validator-0J9XAADH.js} +2 -2
- package/dist/address-book-address-validator-DK0dnsRS.cjs +41 -0
- package/dist/address-book-address-validator-Di_ywo8M.js +39 -0
- package/dist/{address-book-address-validator-DNwQGlOB.cjs → address-book-address-validator-Dte_YQ2L.cjs} +2 -2
- package/dist/artefact-loader-BWWYAD7E.cjs +183 -0
- package/dist/artefact-loader-Dik4HIfv.js +160 -0
- package/dist/auto-dismiss-failure-on-success-C-qoJuid.cjs +37 -0
- package/dist/auto-dismiss-failure-on-success-s8MGDEIu.js +31 -0
- package/dist/bip32-D6wd_QMn.cjs +3851 -0
- package/dist/bitcoin-data-provider-BYPSmuZS.js +21 -0
- package/dist/bitcoin-data-provider-w1oEDSU3.cjs +26 -0
- package/dist/bitcoin.cjs +45 -0
- package/dist/bitcoin.d.ts +29906 -1
- package/dist/bitcoin.js +20 -0
- package/dist/{blockfrost-tx-submit-provider-DPTrapz-.js → blockfrost-tx-submit-provider-CBWQ0wK9.js} +5 -157
- package/dist/{blockfrost-tx-submit-provider-BAIzV5o3.cjs → blockfrost-tx-submit-provider-CLNQ5ycR.cjs} +3 -167
- package/dist/bn-d3nSOIf7.cjs +2553 -0
- package/dist/browser-B1eMbDwh.js +7468 -0
- package/dist/bytes.vo-BB7_4JK6.js +23 -0
- package/dist/bytes.vo-BNRquqaO.cjs +34 -0
- package/dist/cardano-DfPwhwm-.js +3 -0
- package/dist/{cardano-network-id.vo-DSxrFk9S.cjs → cardano-network-id.vo-BEDPn6Li.cjs} +1 -1
- package/dist/{cardano-network-id.vo-CK1tNQDg.js → cardano-network-id.vo-Cfpvbod9.js} +1 -1
- package/dist/cardano.cjs +22 -18
- package/dist/cardano.d.ts +16962 -1716
- package/dist/cardano.js +16 -11
- package/dist/client-DXBLHk7K.cjs +808 -0
- package/dist/codecs-Cswkg7Tm.cjs +4051 -0
- package/dist/codecs-Dc1Krpbt.js +3903 -0
- package/dist/{const-C84qHkjp.js → const-BJfIK_G-.js} +4 -4
- package/dist/const-BhYOdznB.cjs +42 -0
- package/dist/const-BsnjG2_Q.cjs +32071 -0
- package/dist/const-CUnCl5U0.js +13 -0
- package/dist/{const-DXk2tFyv.js → const-CjmfjBP3.js} +1 -1
- package/dist/{const-BDEg2Trq.cjs → const-D-eZIS4n.cjs} +4 -4
- package/dist/const-D0CbQlo4.js +154 -0
- package/dist/{const-BY8B3_0_.cjs → const-iwzZEE_w.cjs} +1 -1
- package/dist/contract-1R86jhSw.js +171 -0
- package/dist/contract-MKLzYVkW.cjs +189 -0
- package/dist/create-add-accounts-Brqk84vF.js +102 -0
- package/dist/create-add-accounts-DzXIh7Ot.cjs +114 -0
- package/dist/{delegation-tx-builder-CRpLP6jH.cjs → delegation-tx-builder-Dof_0N_d.cjs} +6 -6
- package/dist/{delegation-tx-builder-CGVqlaFE.js → delegation-tx-builder-Dy3-2Auz.js} +5 -5
- package/dist/{deregistration-tx-builder-DZGwB4cz.cjs → deregistration-tx-builder-CcLwyFpj.cjs} +6 -6
- package/dist/{deregistration-tx-builder-B-CrUyX6.js → deregistration-tx-builder-QkWW_gWl.js} +5 -5
- package/dist/device-commitment-hex.vo-5wFtQvDS.js +26 -0
- package/dist/device-commitment-hex.vo-CT3RmAug.cjs +31 -0
- package/dist/device-readers-CUHYoYOH.js +145 -0
- package/dist/device-readers-CdxS2btd.cjs +168 -0
- package/dist/device-readers-D_JZeNL2.cjs +8 -0
- package/dist/device-readers-DwSNCGPP.js +4 -0
- package/dist/dist-BMi5ZjEq.cjs +43175 -0
- package/dist/dist-BtIq4XFn.cjs +535 -0
- package/dist/dist-C3SHC_Ho.cjs +1394 -0
- package/dist/dist-CykEjYEX.cjs +3814 -0
- package/dist/dist-Dcsxh3be.cjs +3452 -0
- package/dist/dist-DqJfSlTd.cjs +4413 -0
- package/dist/dist-nTZ70qj7.cjs +1306 -0
- package/dist/{earn-rewards-tx-builder-D5sUT5v8.cjs → earn-rewards-tx-builder-BP5JMIKS.cjs} +6 -6
- package/dist/{earn-rewards-tx-builder-BDbTDZmD.js → earn-rewards-tx-builder-Cku4BmNU.js} +5 -5
- package/dist/enc-key-circuits-B7BjgUL9.js +13 -0
- package/dist/enc-key-circuits-BLrZrBVI.cjs +24 -0
- package/dist/english-Byb9M0E_.cjs +2057 -0
- package/dist/envelopes-BGoO_v8m.js +1621 -0
- package/dist/envelopes-CH25R-DN.cjs +1722 -0
- package/dist/errors-BIftM8ZN.cjs +688 -0
- package/dist/errors-DSq5wUwt.js +466 -0
- package/dist/esm-HBSUQw3w.js +384 -0
- package/dist/esm-LZuaZjMf.cjs +389 -0
- package/dist/experimental.cjs +7349 -0
- package/dist/experimental.d.ts +86279 -0
- package/dist/experimental.js +7184 -0
- package/dist/gated-call-CDCVZIK-.cjs +5 -0
- package/dist/gated-call-D-tT7P_t.js +722 -0
- package/dist/gated-call-D_VT2quk.js +4 -0
- package/dist/gated-call-DgJm1Mi_.cjs +787 -0
- package/dist/grant-circuits-DswmW3nI.cjs +71 -0
- package/dist/grant-circuits-Jxb5y1HW.js +42 -0
- package/dist/grant-rules-By07_eDi.js +6824 -0
- package/dist/grant-rules-BzVTWXS7.cjs +7099 -0
- package/dist/grant-spend-B3Md9L0n.js +215 -0
- package/dist/grant-spend-oWmzRsjH.cjs +217 -0
- package/dist/grant-status-B2y49iIu.js +2688 -0
- package/dist/grant-status-DOr3t_u8.cjs +2820 -0
- package/dist/grants-2BZ-qEVQ.js +5 -0
- package/dist/grants-BPoLSiMt.cjs +18 -0
- package/dist/grants-BoI5uwSF.cjs +272 -0
- package/dist/grants-CtofFu-e.js +213 -0
- package/dist/{group-cardano-addresses-by-account-B533oVXR.cjs → group-cardano-addresses-by-account-Bp0g4wEY.cjs} +3 -39
- package/dist/{group-cardano-addresses-by-account-RHKoEskR.js → group-cardano-addresses-by-account-SR8MMrZf.js} +5 -35
- package/dist/{guarded-transaction-signer-Bxl6_XNd.cjs → guarded-transaction-signer-MFpPg98D.cjs} +17 -18
- package/dist/{guarded-transaction-signer-CpKLj8Kp.js → guarded-transaction-signer-P6Rp1faj.js} +8 -15
- package/dist/hex-D9ITYbwt.cjs +263 -0
- package/dist/hex-mkXcV3pN.js +191 -0
- package/dist/http-client-B3N-TMxu.cjs +167 -0
- package/dist/http-client-TRlhDZGh.js +155 -0
- package/dist/identity-BMLuufva.cjs +263 -0
- package/dist/identity-DII6f-h7.js +233 -0
- package/dist/in-memory-wallet-integration-B2xzxORj.cjs +20741 -0
- package/dist/in-memory-wallet-integration-CBRiPjLO.js +130 -0
- package/dist/in-memory-wallet-integration-CULUB5Tf.cjs +240 -0
- package/dist/{in-memory-wallet-integration-D-qn65Kx.js → in-memory-wallet-integration-ChIOirMW.js} +5 -2
- package/dist/{in-memory-wallet-integration-Croiuopx.js → in-memory-wallet-integration-CljprQQV.js} +7 -104
- package/dist/{in-memory-wallet-integration-DVh9sRXa.cjs → in-memory-wallet-integration-Cm6P8sk6.cjs} +3 -2
- package/dist/in-memory-wallet-integration-TM9bBphA.js +20738 -0
- package/dist/{in-memory-wallet-integration-CnLYeX4-.cjs → in-memory-wallet-integration-t9I00R_V.cjs} +17 -108
- package/dist/index.cjs +84 -47
- package/dist/index.d.ts +37505 -10036
- package/dist/index.js +52 -24
- package/dist/{init-CLHuAMB-.js → init-0zzs9aqU.js} +4 -4
- package/dist/{init-U8c9yHYx.cjs → init-5KcGcBb5.cjs} +1 -1
- package/dist/{init-CigestoM.js → init-B3-1ghyx.js} +2 -2
- package/dist/init-B8U3ABhS.cjs +107 -0
- package/dist/init-BJn-8_hf.js +141 -0
- package/dist/init-BPU3j8sT.cjs +6 -0
- package/dist/init-BRzH6xu-2.cjs +2797 -0
- package/dist/init-BmzfDaKL.js +1142 -0
- package/dist/{init-C-jErzp5.cjs → init-BopV9ed4.cjs} +2 -2
- package/dist/init-BpHTq2R5.js +105 -0
- package/dist/init-BpSITBfF.js +39 -0
- package/dist/{init-DX4WyB0R.cjs → init-BrdVzxei.cjs} +3 -3
- package/dist/{init-LfyMo-sS.cjs → init-BwkU0ruo.cjs} +2 -2
- package/dist/{init-CB__aLD7.cjs → init-C-G32J12.cjs} +2 -2
- package/dist/init-C00gw1zi.js +422 -0
- package/dist/{init-TkPq_Ny_.js → init-C2RgrfZe.js} +1 -1
- package/dist/init-C5mYyFTv.js +3584 -0
- package/dist/{init-DcYY6zMD.js → init-C8URA2OX.js} +1 -1
- package/dist/init-CDkRynJa.js +2796 -0
- package/dist/{init-Cg9XdAru.js → init-CNM5m7M2.js} +4 -4
- package/dist/init-CR7BfztQ.cjs +18 -0
- package/dist/{init-CSHwHnfg.cjs → init-CSDpdu0Q.cjs} +2 -2
- package/dist/{init-aJHl-7AO.js → init-CX0AMQUn.js} +2 -2
- package/dist/{init-FMGDET-9.js → init-CfTbSEKy.js} +259 -60
- package/dist/{init-CR6tpw1B.js → init-Cigr_tKx.js} +47 -43
- package/dist/init-Clu8pxwQ.cjs +1181 -0
- package/dist/{init-CFYsq82V.js → init-CyAY4Lzl.js} +1 -1
- package/dist/{init-BKhH6mVR.cjs → init-D6BxBs7e.cjs} +1 -1
- package/dist/init-D8OeObDF.cjs +1141 -0
- package/dist/init-D9g1HXYf.cjs +40 -0
- package/dist/{init-CNLGJJRx.js → init-DA7zmptx.js} +1 -1
- package/dist/{init-DkIc5nqF.cjs → init-DFMiP2ZH.cjs} +1 -1
- package/dist/{init-zSzCjoor.js → init-DFjT8YvT.js} +4 -4
- package/dist/init-DGjXwWs2.cjs +143 -0
- package/dist/init-DHrCbpkW.cjs +121 -0
- package/dist/init-DRzJR02G.cjs +3585 -0
- package/dist/{init-B64zCLxG.cjs → init-DUF4ar8P.cjs} +4 -4
- package/dist/init-DlqJuHhl.js +119 -0
- package/dist/{init-y2Ra0vZP.js → init-Dnsv155v.js} +3 -3
- package/dist/init-DuYENxci.js +17 -0
- package/dist/{init-D6DBUsD3.cjs → init-Dv3zKj0r.cjs} +4 -4
- package/dist/{init-BtzMZ7Pk.cjs → init-Dy-05ktr.cjs} +1 -1
- package/dist/{init-BsCBAGWf.cjs → init-EP0CUWNK.cjs} +2 -2
- package/dist/{init-BJVU54pI.cjs → init-EhzD7a8i.cjs} +1 -1
- package/dist/{init-w9GEs73W.js → init-Fw89ZmrR.js} +2 -2
- package/dist/init-GsT2AEa4.cjs +424 -0
- package/dist/{init-Rxohwpmu.js → init-NCjgwCZy.js} +2 -2
- package/dist/{init-DxUHsN53.cjs → init-NdhG_VrF.cjs} +6 -6
- package/dist/{init-DDdhguVC.js → init-SZ01tARX2.js} +2 -2
- package/dist/{init-BAJJzBza.js → init-Y3mkB4XS.js} +2 -2
- package/dist/init-c4l6r-o-.js +5 -0
- package/dist/{init-Dku4b2lG.js → init-kEfUTuG2.js} +1 -1
- package/dist/{init-C7IeJwQR.cjs → init-lzHt4Cli.cjs} +256 -59
- package/dist/init-qSEd_Spq.js +1182 -0
- package/dist/{init-anYu6XB7.cjs → init-rmt7-fQE.cjs} +2 -2
- package/dist/{init-C4Bkib9u.cjs → init-zU-_9Gsp.cjs} +67 -65
- package/dist/{is-retriable-error-BU0e3hVI.js → is-retriable-error-BUiVJDwH.js} +1 -1
- package/dist/{is-retriable-error-CeeuHqDa.cjs → is-retriable-error-Dpt860if.cjs} +1 -1
- package/dist/jubjub-authoriser-D7zDXu3V.js +86 -0
- package/dist/jubjub-authoriser-Dgu6o3Sv.cjs +87 -0
- package/dist/key-source-5oUBFiC7.cjs +583 -0
- package/dist/key-source-B08HukZg.js +2550 -0
- package/dist/keyDerivation-D3k9XyHg.js +4077 -0
- package/dist/keyDerivation-u0nBA93T.cjs +274 -0
- package/dist/{lace-context-CZcNHr4M.js → lace-context-BZciB9c7.js} +4 -4
- package/dist/{lace-context-fJTDghdp.cjs → lace-context-DgR6vLli.cjs} +4 -4
- package/dist/lib--iqvW4O_.js +2354 -0
- package/dist/lib-Yk6Upl5D.cjs +2359 -0
- package/dist/{libsodium-wrappers-UH0LaOUE.js → libsodium-wrappers-BhbOA8dQ.js} +3 -1647
- package/dist/midnight-account-id.vo-0x9X-r3l.cjs +10 -0
- package/dist/midnight-account-id.vo-DjCAGcqc.js +5 -0
- package/dist/midnight-engine-DpemplKO.js +5 -0
- package/dist/midnight-engine-xU1wotne.cjs +6 -0
- package/dist/midnight-wallet-DIsAlSn6.js +18 -0
- package/dist/midnight-wallet-XTZc2li7.cjs +24 -0
- package/dist/midnight.cjs +1408 -0
- package/dist/midnight.d.ts +28934 -0
- package/dist/midnight.js +1352 -0
- package/dist/modules-cardano-D9Gs2j60.cjs +1108 -0
- package/dist/modules-cardano-VIM8fKu-.js +995 -0
- package/dist/node-relay-DWe1U0w2.cjs +49 -0
- package/dist/node-relay-h4i5IlAQ.js +48 -0
- package/dist/outputs-Bd2yZVjx.js +720 -0
- package/dist/outputs-CJoQBIA2.cjs +990 -0
- package/dist/pbkdf2-CIW-oWrm.cjs +1337 -0
- package/dist/pending-operation-B0_P78Jo.cjs +26 -0
- package/dist/pending-operation-BIS6mpjB.js +21 -0
- package/dist/providers-FjhM47BH.cjs +388 -0
- package/dist/providers-WtiSuxFu.js +365 -0
- package/dist/redacted-BmE2aXqr.js +81 -0
- package/dist/redacted-CRZJLS3w.cjs +86 -0
- package/dist/{retry-config-DC5IMaH7.cjs → retry-config-BaCfVC1H.cjs} +1 -1
- package/dist/{retry-config-BcUmMS--.js → retry-config-Dmgt28wM.js} +1 -1
- package/dist/safe-buffer-B_vMqJWU.js +43 -0
- package/dist/safe-buffer-DYOwEfHB.cjs +48 -0
- package/dist/sdk-Cb-gZnS9.js +1241 -0
- package/dist/sdk-DffwmX8b.cjs +1439 -0
- package/dist/sdk-init-Bln9rFtn.js +4138 -0
- package/dist/sdk-init-CYViTJY_.cjs +3841 -0
- package/dist/{seam-cardano-CATqBEyK.js → seam-cardano-B_etH4F4.js} +1 -1
- package/dist/{bytes.vo-TgiwogWF.js → secret-box-DUR0ACYz.js} +3 -25
- package/dist/{bytes.vo-Ck5u40RA.cjs → secret-box-DrjLZuS1.cjs} +1 -35
- package/dist/send-proof-progress-C-ljq1x5.js +30 -0
- package/dist/send-proof-progress-vmsaSfbz.cjs +47 -0
- package/dist/side-effects-BnqYz7Nl.js +768 -0
- package/dist/side-effects-CngaeXi0.cjs +804 -0
- package/dist/sign-bitcoin-transaction-with-seed-D4z62-jV.js +147 -0
- package/dist/sign-bitcoin-transaction-with-seed-DsLYXnCY.cjs +158 -0
- package/dist/signer-3fk8lZtE.cjs +7 -0
- package/dist/signer-CsL28DrK.cjs +121 -0
- package/dist/signer-DGluibtn.js +92 -0
- package/dist/signer-RqB5pu3K.js +2 -0
- package/dist/signer-factory-BA7WFJVZ.js +282 -0
- package/dist/signer-factory-Cwd6o-lG.cjs +281 -0
- package/dist/{signer-factory-9URFliA5.js → signer-factory-DObB55D4.js} +6 -3
- package/dist/signer-factory-DhZiE03y.cjs +149 -0
- package/dist/{signer-factory-BWGUmnvm.cjs → signer-factory-Ds6G2AZk.cjs} +4 -3
- package/dist/signer-factory-hzSRaIw2.js +148 -0
- package/dist/{slice-CuHUaPZx.cjs → slice-7G2xxYKJ.cjs} +3 -3
- package/dist/{slice-DZmQJNV8.cjs → slice-B4CdYoPu.cjs} +2 -2
- package/dist/slice-BPNuHKfw.js +9091 -0
- package/dist/{slice-DGcNrbPR.cjs → slice-Bg6HEr3K.cjs} +4 -4
- package/dist/{slice-DtaO72qY.cjs → slice-Bs2aMERu.cjs} +3 -3
- package/dist/{slice-HeHCv3bl.cjs → slice-C34iOOUH.cjs} +7 -7
- package/dist/{slice-Do2R2CYW.cjs → slice-C9TT5KhY.cjs} +6 -6
- package/dist/{slice-eIwcwKHI.js → slice-CK07EBCo.js} +4 -4
- package/dist/{slice-CnyWevM9.js → slice-CU80pbiu.js} +3 -3
- package/dist/{slice-CobanYgn.js → slice-CwyYrh1H.js} +2 -2
- package/dist/slice-Czfp6t84.cjs +436 -0
- package/dist/{slice-d0p6-xj5.js → slice-D-9aPKG-.js} +6 -6
- package/dist/{slice-maaskdUn.js → slice-DBpJ2QGf.js} +6 -6
- package/dist/{slice-mxLQ_FeY.cjs → slice-DTZTiJle.cjs} +352 -39
- package/dist/{slice-zJ9XVYjZ.js → slice-Da426sf9.js} +1 -1
- package/dist/{slice-D11m2EKl.cjs → slice-DayJvMRD.cjs} +1 -1
- package/dist/slice-DnBWfA-k.cjs +9109 -0
- package/dist/{slice-oqm_r1cS.cjs → slice-DnG-yvpI.cjs} +2 -2
- package/dist/{slice-BNu73oBa.js → slice-DyNs7oWp.js} +2 -2
- package/dist/{slice-CB4SnzMm.js → slice-GwiiJRoq.js} +3 -3
- package/dist/slice-oWnc7YVD.js +412 -0
- package/dist/{slice-B2PhoWAg.js → slice-qt0uBExh.js} +325 -37
- package/dist/slice-t8Gzj55G2.cjs +106 -0
- package/dist/slice-t8Q1cHm-.js +88 -0
- package/dist/src-BDzYIksE.cjs +20637 -0
- package/dist/src-BFoefPv_.js +20755 -0
- package/dist/store-B1Lq3cEf.cjs +66 -0
- package/dist/store-DuBG8qh5.js +37 -0
- package/dist/submit-Bs77dPn_.cjs +413 -0
- package/dist/submit-DOv8Z0Vf.js +336 -0
- package/dist/submit-bitcoin-tx-D6oYgH5S.js +983 -0
- package/dist/submit-bitcoin-tx-goFuRL7K.cjs +1049 -0
- package/dist/{sync-operation-utils-BYXLO80m.js → sync-operation-utils-31iFLExW.js} +2 -38
- package/dist/{sync-operation-utils-SdPbwFoD.cjs → sync-operation-utils-COIZ66-I.cjs} +1 -67
- package/dist/token-id-mapper-Bnds3xKH.cjs +18 -0
- package/dist/{token-id-mapper-DsPAL6Nh.cjs → token-id-mapper-CVyQJUKY.cjs} +2 -2
- package/dist/token-id-mapper-CnKHQ8DV.js +16 -0
- package/dist/{token-id-mapper-B2ivlLv3.js → token-id-mapper-D29Ggv8h.js} +2 -2
- package/dist/tokens-Cevp7rdU.cjs +62 -0
- package/dist/tokens-Diw4MLRM.js +44 -0
- package/dist/types-B8uiJn-X.js +12 -0
- package/dist/types-BHC12eSa.js +33 -0
- package/dist/types-CJT6XF2c.js +10 -0
- package/dist/types-CLRUYpfe.cjs +23 -0
- package/dist/types-Cc3mC8u0.cjs +50 -0
- package/dist/types-DWbVZpnF.cjs +15 -0
- package/dist/unwrap-provider-result-BZSiwabU.js +32 -0
- package/dist/unwrap-provider-result-ZqfBsTkk.cjs +39 -0
- package/dist/{util-CGzqQ8IV.cjs → util-Bu4W5lF4.cjs} +1 -1
- package/dist/{util-BaLzwa32.js → util-Cp5cZkfK.js} +1 -1
- package/dist/{utils-o8aMjfln.js → utils-BfBEUvWA.js} +2 -2
- package/dist/utils-DFwzEvbv.js +65 -0
- package/dist/{utils-5JvjbqH3.cjs → utils-DsElZgCI.cjs} +7 -1
- package/dist/utils-IXxR-Djh.cjs +112 -0
- package/dist/v8Signatures-CO_Uloy7.cjs +71 -0
- package/dist/{vote-delegation-tx-builder-CmKv6GoG.js → vote-delegation-tx-builder-BsBSDmcX.js} +5 -5
- package/dist/{vote-delegation-tx-builder-BpEMSti4.cjs → vote-delegation-tx-builder-DSs8nybk.cjs} +6 -6
- package/dist/watch-CUEvcP-V.js +21997 -0
- package/dist/watch-DMF2s-II.cjs +52281 -0
- package/package.json +96 -4
- package/dist/account-guards.stable-B1DSSh9D.cjs +0 -10
- package/dist/account-guards.stable-JRQ5qx2V.js +0 -5
- package/dist/codecs-B7X9pAsK.cjs +0 -1945
- package/dist/codecs-CC1Xjntz.js +0 -1882
- package/dist/init-Bv53hpLM.js +0 -19
- package/dist/init-CRbjI7p1.js +0 -3494
- package/dist/init-DAna1KCg.cjs +0 -3495
- package/dist/init-DGe47B22.cjs +0 -364
- package/dist/init-DMtyvYfC.cjs +0 -21
- package/dist/init-TkPDD750.js +0 -363
- package/dist/modules-cardano-BDvuL4Xx.js +0 -5248
- package/dist/modules-cardano-D2G3De2P.cjs +0 -5432
- package/dist/slice-CGVHXHDt.cjs +0 -125
- package/dist/slice-DhvF7L6X.js +0 -107
- /package/dist/{account-id.vo-LvCBVn5r.cjs → account-id.vo-DwqFjK5u.cjs} +0 -0
- /package/dist/{account-id.vo-MYSI7YFm.js → account-id.vo-OPz2v-JZ.js} +0 -0
- /package/dist/{actions-DdGbj7c7.cjs → actions-B2BJ4RTy.cjs} +0 -0
- /package/dist/{actions-9QSQR-GB.js → actions-iLXdgL5Y.js} +0 -0
- /package/dist/{apply-vkey-witnesses-ClZbpk5w.js → apply-vkey-witnesses-BKx3lcs0.js} +0 -0
- /package/dist/{apply-vkey-witnesses-CLzVSWxl.cjs → apply-vkey-witnesses-ZnFS0jnJ.cjs} +0 -0
- /package/dist/{asyncoption-Cw07zPH7.js → asyncoption-CruEYOBI.js} +0 -0
- /package/dist/{asyncoption-DYoGuQfP.cjs → asyncoption-i0VbqVTq.cjs} +0 -0
- /package/dist/{bignum.vo-B0rK_PQ1.js → bignum.vo-CEXOIew0.js} +0 -0
- /package/dist/{bignum.vo-DsG3kNiZ.cjs → bignum.vo-hx2kuqlA.cjs} +0 -0
- /package/dist/{bip32ed25519-1ApqeAZk.cjs → bip32ed25519-B7Yat217.cjs} +0 -0
- /package/dist/{bip32ed25519-DetAdGwD.js → bip32ed25519-FQ8qvdcq.js} +0 -0
- /package/dist/{blake2b-DOEVc57D.js → blake2b-BiQTw3GC.js} +0 -0
- /package/dist/{blake2b-B9tftbvG.cjs → blake2b-DEeVUdKR.cjs} +0 -0
- /package/dist/{blockchain-network-id.vo-BXpZhvvQ.cjs → blockchain-network-id.vo-BAKEwGjG.cjs} +0 -0
- /package/dist/{blockchain-network-id.vo-DNRkuT5B.js → blockchain-network-id.vo-CiVykuqG.js} +0 -0
- /package/dist/{blockingWithLatestFrom-C5wp_uA-.js → blockingWithLatestFrom-Cd8c8EPG.js} +0 -0
- /package/dist/{blockingWithLatestFrom-DY8OPg0w.cjs → blockingWithLatestFrom-D8kBPBMX.cjs} +0 -0
- /package/dist/{cardano-network-id-to-network-type-CrZfUcyP.js → cardano-network-id-to-network-type-BWwzeEtM.js} +0 -0
- /package/dist/{cardano-network-id-to-network-type-BDTF-HmK.cjs → cardano-network-id-to-network-type-C0ufJOyE.cjs} +0 -0
- /package/dist/{cardano-tx-id.vo-BPhV-AmZ.cjs → cardano-tx-id.vo-TLjJZHCI.cjs} +0 -0
- /package/dist/{cardano-tx-id.vo-DF9NzZXk.js → cardano-tx-id.vo-eVnKuzlm.js} +0 -0
- /package/dist/{composite-signer-factory-_46QsNBz.js → composite-signer-factory-CR8YHA1y.js} +0 -0
- /package/dist/{composite-signer-factory-BpF1mIYA.cjs → composite-signer-factory-m5cVs9h_.cjs} +0 -0
- /package/dist/{error-CNIquqT5.cjs → error-Cb3EnD0u.cjs} +0 -0
- /package/dist/{error-BCp9NkPi.js → error-DIZOY2Lw.js} +0 -0
- /package/dist/{esm-browser-C93Q-EOI.js → esm-browser-MG97cre9.js} +0 -0
- /package/dist/{esm-browser-DTc9IK3D.cjs → esm-browser-bweqfBlL.cjs} +0 -0
- /package/dist/{feature-flag-key.vo-ofrB6ZVQ.js → feature-flag-key.vo-DN_-4-sG.js} +0 -0
- /package/dist/{feature-flag-key.vo-BPz-ZAp6.cjs → feature-flag-key.vo-RlfHL3hq.cjs} +0 -0
- /package/dist/{first-state-of-status-OROkXcRv.cjs → first-state-of-status-CbT7hPa4.cjs} +0 -0
- /package/dist/{first-state-of-status-DQnMT3ya.js → first-state-of-status-unPynOuM.js} +0 -0
- /package/dist/{init-Es3wHW7D.js → init-829HRYcz.js} +0 -0
- /package/dist/{init-CKipPE_c.js → init-A-VyGSNW2.js} +0 -0
- /package/dist/{init-DFQQ9dTW.cjs → init-Atisw4x2.cjs} +0 -0
- /package/dist/{init-C5CEZ1m0.cjs → init-BFcvPHL-.cjs} +0 -0
- /package/dist/{init-IAk_CkGa.cjs → init-BgNobbNq.cjs} +0 -0
- /package/dist/{init-cqN3uX2j.js → init-atCFur0v.js} +0 -0
- /package/dist/{is-redux-persist-state-CTn3WZVh.cjs → is-redux-persist-state-BCFnSm36.cjs} +0 -0
- /package/dist/{is-redux-persist-state-Cxcd0_bM.js → is-redux-persist-state-yBfq0h_E.js} +0 -0
- /package/dist/{key-value-store-8cMzeNyG.js → key-value-store--W3SrMaw.js} +0 -0
- /package/dist/{key-value-store-DUAepXQf.cjs → key-value-store-C2Xt5Emp.cjs} +0 -0
- /package/dist/{repo-slice-WxetRhMY.cjs → repo-slice-7X4AYYVm.cjs} +0 -0
- /package/dist/{repo-slice-DwyMhCR1.js → repo-slice-RxFm0zDZ.js} +0 -0
- /package/dist/{reselect-CRDqmQY9.cjs → reselect-Bd94u0K-.cjs} +0 -0
- /package/dist/{reselect-C06X06oX.js → reselect-CGoLzEPv.js} +0 -0
- /package/dist/{serializable-B8GahgE1.js → serializable-BHdKIgk3.js} +0 -0
- /package/dist/{serializable-CRdnFaLZ.cjs → serializable-BxHkoPdk.cjs} +0 -0
- /package/dist/{slice-D11FHQuQ.cjs → slice-BiRsDKOK.cjs} +0 -0
- /package/dist/{slice-Be7hj4Iy.js → slice-Bk0Sv05J.js} +0 -0
- /package/dist/{slice-FioKeHtA.cjs → slice-Dqw1ecum.cjs} +0 -0
- /package/dist/{slice-B87b52iM.js → slice-obCSLm1x.js} +0 -0
- /package/dist/{src-vq1wS9sZ.js → src-BlxEp6Mm.js} +0 -0
- /package/dist/{src-BTPIvlDI.cjs → src-Cqp0B2M_.cjs} +0 -0
- /package/dist/{time.vo-CQ8pSXz2.cjs → time.vo-D3wWRw9L.cjs} +0 -0
- /package/dist/{time.vo-DkYYGYp6.js → time.vo-tNheinXm.js} +0 -0
- /package/dist/{toEmpty-CgAl3097.js → toEmpty-8Zax1MPd.js} +0 -0
- /package/dist/{toEmpty-DAMmNB5r.cjs → toEmpty-CYAbupM4.cjs} +0 -0
- /package/dist/{token-id.vo-ClsroxHO.js → token-id.vo-Bvwhr3ZE.js} +0 -0
- /package/dist/{token-id.vo-BxG9TG0O.cjs → token-id.vo-DXv5uS9n.cjs} +0 -0
- /package/dist/{util-DxzlYNS5.js → util-C8a4fA9p.js} +0 -0
- /package/dist/{util-CmdAhheh.cjs → util-CzeB3hfa.cjs} +0 -0
- /package/dist/{util-selector-CcK-GAdE.js → util-selector-BSTsxgne.js} +0 -0
- /package/dist/{util-selector-BcAncU4L.cjs → util-selector-BWPH68_l.cjs} +0 -0
- /package/dist/{while-active-BpppWuCs.js → while-active-B0lWy_6l.js} +0 -0
- /package/dist/{while-active-Dhc2LwOm.cjs → while-active-Dsowp0Fi.cjs} +0 -0
|
@@ -0,0 +1,3585 @@
|
|
|
1
|
+
require("./rolldown-runtime-Br2OG8yv.cjs");
|
|
2
|
+
const require_util = require("./util-Bu4W5lF4.cjs");
|
|
3
|
+
const require_bignum_vo = require("./bignum.vo-hx2kuqlA.cjs");
|
|
4
|
+
const require_serializable = require("./serializable-BxHkoPdk.cjs");
|
|
5
|
+
require("./retry-config-BaCfVC1H.cjs");
|
|
6
|
+
const require_account = require("./account-BUOj4pYJ.cjs");
|
|
7
|
+
const require_hex = require("./hex-D9ITYbwt.cjs");
|
|
8
|
+
const require_outputs = require("./outputs-CJoQBIA2.cjs");
|
|
9
|
+
const require_tokens = require("./tokens-Cevp7rdU.cjs");
|
|
10
|
+
const require_grant_status = require("./grant-status-DOr3t_u8.cjs");
|
|
11
|
+
const require_identity = require("./identity-BMLuufva.cjs");
|
|
12
|
+
const require_TransactionBuilder = require("./TransactionBuilder-X1H9Gze6.cjs");
|
|
13
|
+
const require_is_retriable_error = require("./is-retriable-error-Dpt860if.cjs");
|
|
14
|
+
const require_unwrap_provider_result = require("./unwrap-provider-result-ZqfBsTkk.cjs");
|
|
15
|
+
let _laceio_cardano_crypto = require("@laceio/cardano/crypto");
|
|
16
|
+
let _laceio_cardano_core = require("@laceio/cardano/core");
|
|
17
|
+
let _laceio_cardano_util = require("@laceio/cardano/util");
|
|
18
|
+
let rxjs = require("rxjs");
|
|
19
|
+
let _laceio_cardano_tx_construction = require("@laceio/cardano/tx-construction");
|
|
20
|
+
//#region ../../packages/lib/cardano-custody/src/analysis/sponsor-rules.ts
|
|
21
|
+
/** The service's limit on the size of a transaction it inspects, in bytes. */
|
|
22
|
+
const MAX_TRANSACTION_BYTES = 16 * 1024;
|
|
23
|
+
/** The service's default limit on the fee a leased fee UTxO pays, in lovelace. */
|
|
24
|
+
const MAX_FEE_LOVELACE = 2000000n;
|
|
25
|
+
/**
|
|
26
|
+
* The most devices a well formed state lists, which bounds the stake scripts
|
|
27
|
+
* a creation has the service derive.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_DEVICES = 8;
|
|
30
|
+
const outpointKey = ({ txId, index }) => `${txId.toLowerCase()}#${index}`;
|
|
31
|
+
const toTxIn = ({ txId, index }) => ({
|
|
32
|
+
txId,
|
|
33
|
+
index
|
|
34
|
+
});
|
|
35
|
+
const paymentCredentialOf = (address) => {
|
|
36
|
+
const parsed = _laceio_cardano_core.Cardano.Address.fromString(address);
|
|
37
|
+
return (parsed?.asBase() ?? parsed?.asEnterprise() ?? parsed?.asPointer())?.getPaymentCredential();
|
|
38
|
+
};
|
|
39
|
+
const stakeCredentialOf = (address) => {
|
|
40
|
+
const parsed = _laceio_cardano_core.Cardano.Address.fromString(address);
|
|
41
|
+
return parsed?.asBase()?.getStakeCredential() ?? parsed?.asReward()?.getPaymentCredential();
|
|
42
|
+
};
|
|
43
|
+
const keyHashOf = (credential) => credential?.type === _laceio_cardano_core.Cardano.CredentialType.KeyHash ? credential.hash : void 0;
|
|
44
|
+
const scriptHashOf$1 = (credential) => credential?.type === _laceio_cardano_core.Cardano.CredentialType.ScriptHash ? credential.hash : void 0;
|
|
45
|
+
const sponsorKeysOf = (address) => ({
|
|
46
|
+
paymentKeyHash: keyHashOf(paymentCredentialOf(address)),
|
|
47
|
+
stakeKeyHash: keyHashOf(stakeCredentialOf(address))
|
|
48
|
+
});
|
|
49
|
+
const namesSponsorKey = (credential, { paymentKeyHash, stakeKeyHash }) => credential.type === _laceio_cardano_core.Cardano.CredentialType.KeyHash && (credential.hash === paymentKeyHash || credential.hash === stakeKeyHash);
|
|
50
|
+
const isAccountOutput = (output, { contractVersion }) => scriptHashOf$1(paymentCredentialOf(output.address)) === contractVersion.accountValidatorHash;
|
|
51
|
+
const policyIdOf = (assetId) => _laceio_cardano_core.Cardano.AssetId.getPolicyId(assetId);
|
|
52
|
+
/** The 28-byte hash a datum field holds, or undefined for a field of any other shape. */
|
|
53
|
+
const hashFieldOf = (field) => _laceio_cardano_core.Cardano.util.isPlutusBoundedBytes(field) && field.length === 28 ? (0, _laceio_cardano_crypto.Hash28ByteBase16)(require_hex.bytesToHex(field)) : void 0;
|
|
54
|
+
/** The fields of an inline datum that is constructor 0, or undefined for any other datum. */
|
|
55
|
+
const inlineFieldsOf = (output) => _laceio_cardano_core.Cardano.util.isConstrPlutusData(output.datum) && output.datum.constructor === 0n ? output.datum.fields.items : void 0;
|
|
56
|
+
/** The logic a control datum names: a 28-byte hash in its first field. */
|
|
57
|
+
const logicHashOf = (output) => hashFieldOf(inlineFieldsOf(output)?.[0]);
|
|
58
|
+
/** The devices a control datum lists: a list of 28-byte hashes in its second field. */
|
|
59
|
+
const devicesOf = (output) => {
|
|
60
|
+
const list = inlineFieldsOf(output)?.[1];
|
|
61
|
+
if (!_laceio_cardano_core.Cardano.util.isPlutusList(list)) return void 0;
|
|
62
|
+
const devices = list.items.map(hashFieldOf);
|
|
63
|
+
return devices.every((device) => device !== void 0) ? devices : void 0;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* The account token an input holds, when it is a control or a grant UTxO:
|
|
67
|
+
* the output sits at an account address staked to a script and holds a
|
|
68
|
+
* token under the account policy named after that very script, alone or
|
|
69
|
+
* followed by a slot. The validator mints a token only into such an
|
|
70
|
+
* output, so an output shaped otherwise is never read as an account's. The
|
|
71
|
+
* control token wins when an output holds both kinds.
|
|
72
|
+
*/
|
|
73
|
+
const accountTokenOf = ({ input, output }, context) => {
|
|
74
|
+
if (output === void 0 || !isAccountOutput(output, context)) return;
|
|
75
|
+
const account = scriptHashOf$1(stakeCredentialOf(output.address));
|
|
76
|
+
if (account === void 0) return void 0;
|
|
77
|
+
const kinds = [...output.value.assets ?? /* @__PURE__ */ new Map()].flatMap(([assetId, quantity]) => {
|
|
78
|
+
if (policyIdOf(assetId) !== context.contractVersion.accountValidatorHash || quantity <= 0n) return [];
|
|
79
|
+
const name = require_tokens.parseCustodyAccountTokenName(_laceio_cardano_core.Cardano.AssetId.getAssetName(assetId));
|
|
80
|
+
return name?.stakeScriptHash === account ? [name.kind] : [];
|
|
81
|
+
});
|
|
82
|
+
if (kinds.length === 0) return void 0;
|
|
83
|
+
return {
|
|
84
|
+
input,
|
|
85
|
+
output,
|
|
86
|
+
account,
|
|
87
|
+
kind: kinds.includes("state-nft") ? "control" : "grant"
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The account a control output belongs to: an output at an account address
|
|
92
|
+
* staked to a script, holding exactly one state NFT named after that script.
|
|
93
|
+
* The validator pins the state NFT to such an output, so its datum names
|
|
94
|
+
* the logic the account runs once the transaction lands.
|
|
95
|
+
*/
|
|
96
|
+
const controlOutputAccount = (output, context) => {
|
|
97
|
+
if (!isAccountOutput(output, context)) return void 0;
|
|
98
|
+
const account = scriptHashOf$1(stakeCredentialOf(output.address));
|
|
99
|
+
if (account === void 0) return void 0;
|
|
100
|
+
const stateNft = _laceio_cardano_core.Cardano.AssetId.fromParts(_laceio_cardano_core.Cardano.PolicyId(context.contractVersion.accountValidatorHash), _laceio_cardano_core.Cardano.AssetName(account));
|
|
101
|
+
return output.value.assets?.get(stateNft) === 1n ? account : void 0;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Every logic the transaction names: the one each control UTxO it spends or
|
|
105
|
+
* references names, then the one each control output it writes names. A
|
|
106
|
+
* creation names its initial logic in its control output, and an upgrade
|
|
107
|
+
* names the logic it leaves in the control UTxO and the one it arrives at in
|
|
108
|
+
* the control output.
|
|
109
|
+
*/
|
|
110
|
+
const namedLogicsOf = ({ spent, referencedControls }, body, context) => [...[...spent.filter(({ kind }) => kind === "control"), ...referencedControls].map(({ input, output, account }) => ({
|
|
111
|
+
logic: logicHashOf(output),
|
|
112
|
+
control: {
|
|
113
|
+
kind: "control-utxo",
|
|
114
|
+
input
|
|
115
|
+
},
|
|
116
|
+
stakeScriptHash: account
|
|
117
|
+
})), ...body.outputs.flatMap((output, outputIndex) => {
|
|
118
|
+
const account = controlOutputAccount(output, context);
|
|
119
|
+
return account === void 0 ? [] : [{
|
|
120
|
+
logic: logicHashOf(output),
|
|
121
|
+
control: {
|
|
122
|
+
kind: "control-output",
|
|
123
|
+
outputIndex
|
|
124
|
+
},
|
|
125
|
+
stakeScriptHash: account
|
|
126
|
+
}];
|
|
127
|
+
})];
|
|
128
|
+
/**
|
|
129
|
+
* A sponsor UTxO is the leased fee UTxO, the shared collateral, or any
|
|
130
|
+
* output locked by the sponsor payment key: the sponsor's signature
|
|
131
|
+
* authorises every one of them.
|
|
132
|
+
*/
|
|
133
|
+
const isSponsorInput = ({ input, output }, context, sponsorKeys) => {
|
|
134
|
+
const key = outpointKey(input);
|
|
135
|
+
return context.mode.kind === "fee" && key === outpointKey(context.mode.feeUtxo[0]) || key === outpointKey(context.collateralUtxo[0]) || output !== void 0 && sponsorKeys.paymentKeyHash !== void 0 && keyHashOf(paymentCredentialOf(output.address)) === sponsorKeys.paymentKeyHash;
|
|
136
|
+
};
|
|
137
|
+
/** Every credential a certificate names, any of which may have to sign. */
|
|
138
|
+
const certificateCredentials = (certificate) => {
|
|
139
|
+
const named = [];
|
|
140
|
+
if ("stakeCredential" in certificate) named.push(certificate.stakeCredential);
|
|
141
|
+
if ("dRep" in certificate && _laceio_cardano_core.Cardano.isDRepCredential(certificate.dRep)) named.push(certificate.dRep);
|
|
142
|
+
if ("dRepCredential" in certificate) named.push(certificate.dRepCredential);
|
|
143
|
+
if ("coldCredential" in certificate) named.push(certificate.coldCredential);
|
|
144
|
+
if ("hotCredential" in certificate) named.push(certificate.hotCredential);
|
|
145
|
+
const poolIds = [];
|
|
146
|
+
if ("poolId" in certificate) poolIds.push(certificate.poolId);
|
|
147
|
+
if ("poolParameters" in certificate) {
|
|
148
|
+
const { id, rewardAccount, owners } = certificate.poolParameters;
|
|
149
|
+
poolIds.push(id);
|
|
150
|
+
named.push(stakeCredentialOf(rewardAccount), ...owners.map((owner) => stakeCredentialOf(owner)));
|
|
151
|
+
}
|
|
152
|
+
for (const poolId of poolIds) named.push({
|
|
153
|
+
type: _laceio_cardano_core.Cardano.CredentialType.KeyHash,
|
|
154
|
+
hash: _laceio_cardano_core.Cardano.PoolId.toKeyHash(poolId)
|
|
155
|
+
});
|
|
156
|
+
return named.filter((credential) => credential !== void 0);
|
|
157
|
+
};
|
|
158
|
+
const LOVELACE_KEY = "";
|
|
159
|
+
/** Lovelace and every asset a list of values holds, keyed by asset id ('' for lovelace). */
|
|
160
|
+
const balanceOf = (values) => {
|
|
161
|
+
const balance = /* @__PURE__ */ new Map();
|
|
162
|
+
const add = (key, quantity) => balance.set(key, (balance.get(key) ?? 0n) + quantity);
|
|
163
|
+
for (const value of values) {
|
|
164
|
+
add(LOVELACE_KEY, value.coins);
|
|
165
|
+
for (const [assetId, quantity] of value.assets ?? []) add(assetId, quantity);
|
|
166
|
+
}
|
|
167
|
+
return balance;
|
|
168
|
+
};
|
|
169
|
+
/** Reads each entry of the CBOR map the reader is at, definite or not. */
|
|
170
|
+
const readMapEntries = (reader, readEntry) => {
|
|
171
|
+
const count = reader.readStartMap();
|
|
172
|
+
for (let read = 0; count === null ? reader.peekState() !== _laceio_cardano_core.Serialization.CborReaderState.EndMap : read < count; read++) readEntry();
|
|
173
|
+
};
|
|
174
|
+
/** The fields of an encoded body or witness set by key, each as the bytes it is stated in. */
|
|
175
|
+
const fieldsOf = (encoded) => {
|
|
176
|
+
const reader = new _laceio_cardano_core.Serialization.CborReader(require_hex.bytesToHex(encoded));
|
|
177
|
+
const fields = /* @__PURE__ */ new Map();
|
|
178
|
+
readMapEntries(reader, () => fields.set(reader.readUInt(), require_hex.bytesToHex(reader.readEncodedValue())));
|
|
179
|
+
return fields;
|
|
180
|
+
};
|
|
181
|
+
const BODY_WITHDRAWALS = 5n;
|
|
182
|
+
const WITNESS_DATUMS = 4n;
|
|
183
|
+
const WITNESS_REDEEMERS = 5n;
|
|
184
|
+
/**
|
|
185
|
+
* The body withdrawals and the witness script data as the transaction's
|
|
186
|
+
* bytes state them, which is what the service reads: the decoded body keeps
|
|
187
|
+
* one withdrawal per reward account, and the script data hash is taken over
|
|
188
|
+
* the bytes the witness set carries.
|
|
189
|
+
*/
|
|
190
|
+
const statedOf = (transaction) => {
|
|
191
|
+
const reader = new _laceio_cardano_core.Serialization.CborReader((0, _laceio_cardano_util.HexBlob)(transaction.toCbor()));
|
|
192
|
+
reader.readStartArray();
|
|
193
|
+
const body = fieldsOf(reader.readEncodedValue());
|
|
194
|
+
const witnessSet = fieldsOf(reader.readEncodedValue());
|
|
195
|
+
const withdrawals = [];
|
|
196
|
+
const encodedWithdrawals = body.get(BODY_WITHDRAWALS);
|
|
197
|
+
if (encodedWithdrawals !== void 0) {
|
|
198
|
+
const withdrawalReader = new _laceio_cardano_core.Serialization.CborReader(encodedWithdrawals);
|
|
199
|
+
readMapEntries(withdrawalReader, () => withdrawals.push({
|
|
200
|
+
stakeAddress: _laceio_cardano_core.Cardano.RewardAccount(_laceio_cardano_core.Cardano.Address.fromBytes(require_hex.bytesToHex(withdrawalReader.readByteString())).toBech32()),
|
|
201
|
+
quantity: withdrawalReader.readUInt()
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
withdrawals,
|
|
206
|
+
witnessScriptData: {
|
|
207
|
+
redeemers: witnessSet.get(WITNESS_REDEEMERS),
|
|
208
|
+
datums: witnessSet.get(WITNESS_DATUMS)
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Reads an account creation, or the reason the transaction is none:
|
|
214
|
+
* exactly one token minted under the account policy, named with the 28
|
|
215
|
+
* bytes of a stake script hash, exactly one registration of a script stake
|
|
216
|
+
* credential with its deposit, the token named after that credential and
|
|
217
|
+
* held by exactly one output at an account address staked to it, and that
|
|
218
|
+
* credential the version's stake script of a device the output's datum
|
|
219
|
+
* lists. A datum listing more devices than a well formed state is refused
|
|
220
|
+
* before any script is derived.
|
|
221
|
+
*/
|
|
222
|
+
const readCreation = (body, context) => {
|
|
223
|
+
const refuse = (reason) => ({
|
|
224
|
+
rule: "account_transaction",
|
|
225
|
+
reason
|
|
226
|
+
});
|
|
227
|
+
const { accountValidatorHash } = context.contractVersion;
|
|
228
|
+
const minted = [...body.mint ?? /* @__PURE__ */ new Map()].filter(([assetId]) => policyIdOf(assetId) === accountValidatorHash);
|
|
229
|
+
if (minted.length === 0) return refuse("not-account-transaction");
|
|
230
|
+
const [[assetId, quantity]] = minted;
|
|
231
|
+
if (minted.length !== 1 || quantity !== 1n) return refuse("mint-count");
|
|
232
|
+
if (require_tokens.parseCustodyAccountTokenName(_laceio_cardano_core.Cardano.AssetId.getAssetName(assetId))?.kind !== "state-nft") return refuse("state-nft-name-length");
|
|
233
|
+
const registrations = (body.certificates ?? []).filter((certificate) => certificate.__typename === _laceio_cardano_core.Cardano.CertificateType.Registration && certificate.stakeCredential.type === _laceio_cardano_core.Cardano.CredentialType.ScriptHash);
|
|
234
|
+
if (registrations.length !== 1) return refuse("registration-count");
|
|
235
|
+
const [registration] = registrations;
|
|
236
|
+
const stakeScriptHash = registration.stakeCredential.hash;
|
|
237
|
+
if (_laceio_cardano_core.Cardano.AssetId.getAssetName(assetId) !== stakeScriptHash) return refuse("state-nft-name");
|
|
238
|
+
const holders = body.outputs.filter((output) => output.value.assets?.get(assetId) === 1n);
|
|
239
|
+
if (holders.length !== 1 || !isAccountOutput(holders[0], context)) return refuse("control-output-placement");
|
|
240
|
+
const [controlOutput] = holders;
|
|
241
|
+
if (scriptHashOf$1(stakeCredentialOf(controlOutput.address)) !== stakeScriptHash) return refuse("control-output-stake");
|
|
242
|
+
const devices = devicesOf(controlOutput);
|
|
243
|
+
if (devices === void 0) return refuse("control-output-devices");
|
|
244
|
+
if (devices.length > MAX_DEVICES) return {
|
|
245
|
+
rule: "account_transaction",
|
|
246
|
+
reason: "device-count",
|
|
247
|
+
count: devices.length,
|
|
248
|
+
limit: MAX_DEVICES
|
|
249
|
+
};
|
|
250
|
+
const chainId = require_identity.chainIdFromNetworkMagic(context.networkMagic);
|
|
251
|
+
if (!devices.some((device) => require_identity.identityOfValidatedCreator({
|
|
252
|
+
contractVersion: context.contractVersion,
|
|
253
|
+
creatorKeyHash: device,
|
|
254
|
+
chainId
|
|
255
|
+
}).stakeScriptHash === stakeScriptHash)) return {
|
|
256
|
+
rule: "account_transaction",
|
|
257
|
+
reason: "underived-stake-credential",
|
|
258
|
+
stakeScriptHash
|
|
259
|
+
};
|
|
260
|
+
return {
|
|
261
|
+
deposit: registration.deposit,
|
|
262
|
+
controlLovelace: controlOutput.value.coins,
|
|
263
|
+
stakeScriptHash
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
/** The accounts whose control UTxO the transaction spends or references. */
|
|
267
|
+
const controlledAccountsOf = ({ spent, referencedControls }) => new Set([...spent, ...referencedControls].filter(({ kind }) => kind === "control").map(({ account }) => account));
|
|
268
|
+
/**
|
|
269
|
+
* Reads what the transaction does to the account contract. A referenced
|
|
270
|
+
* control UTxO serves only an account the transaction operates, so one of
|
|
271
|
+
* any other account is refused first, even beside a creation. A
|
|
272
|
+
* transaction spending no account token is a creation, or no account
|
|
273
|
+
* transaction at all. One spending a token is an operation, and every
|
|
274
|
+
* account it operates needs its control UTxO spent or referenced, as the
|
|
275
|
+
* contract needs on the owner, the sweep and the agent path alike.
|
|
276
|
+
*/
|
|
277
|
+
const readAccountTransaction = (body, { spent, referencedControls }, context) => {
|
|
278
|
+
const refuse = (reason, { input, account }) => ({
|
|
279
|
+
kind: "refused",
|
|
280
|
+
violation: {
|
|
281
|
+
rule: "account_transaction",
|
|
282
|
+
reason,
|
|
283
|
+
input,
|
|
284
|
+
stakeScriptHash: account
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
const operated = new Set(spent.map(({ account }) => account));
|
|
288
|
+
const foreignControl = referencedControls.find(({ account }) => !operated.has(account));
|
|
289
|
+
if (foreignControl) return refuse("foreign-referenced-control", foreignControl);
|
|
290
|
+
if (spent.length === 0) {
|
|
291
|
+
const creation = readCreation(body, context);
|
|
292
|
+
return "rule" in creation ? {
|
|
293
|
+
kind: "refused",
|
|
294
|
+
violation: creation
|
|
295
|
+
} : {
|
|
296
|
+
kind: "creation",
|
|
297
|
+
creation
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const controlled = controlledAccountsOf({
|
|
301
|
+
spent,
|
|
302
|
+
referencedControls
|
|
303
|
+
});
|
|
304
|
+
const orphan = spent.find(({ account }) => !controlled.has(account));
|
|
305
|
+
return orphan ? refuse("orphan-grant", orphan) : { kind: "operation" };
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* The stake scripts of the accounts the transaction belongs to: those of
|
|
309
|
+
* the control UTxOs it spends or references, which name them, and the one
|
|
310
|
+
* it registers at creation. A grant UTxO alone names none: an agent spend
|
|
311
|
+
* references the control UTxO of its account.
|
|
312
|
+
*/
|
|
313
|
+
const stakeScriptHashesOf = (tokens, reading) => {
|
|
314
|
+
const hashes = controlledAccountsOf(tokens);
|
|
315
|
+
if (reading.kind === "creation") hashes.add(reading.creation.stakeScriptHash);
|
|
316
|
+
return hashes;
|
|
317
|
+
};
|
|
318
|
+
const analyse = (transaction, resolvedInputs, context) => {
|
|
319
|
+
const body = transaction.body().toCore();
|
|
320
|
+
const sponsorKeys = sponsorKeysOf(context.sponsorAddress);
|
|
321
|
+
const resolutions = new Map(resolvedInputs.map(([input, output]) => [outpointKey(input), output]));
|
|
322
|
+
const resolve = (input) => ({
|
|
323
|
+
input: toTxIn(input),
|
|
324
|
+
output: resolutions.get(outpointKey(input))
|
|
325
|
+
});
|
|
326
|
+
const inputs = body.inputs.map(resolve);
|
|
327
|
+
const referenceInputs = (body.referenceInputs ?? []).map(resolve);
|
|
328
|
+
const sponsorInputs = inputs.filter((input) => isSponsorInput(input, context, sponsorKeys));
|
|
329
|
+
const tokens = {
|
|
330
|
+
spent: inputs.filter((input) => !sponsorInputs.includes(input)).flatMap((input) => accountTokenOf(input, context) ?? []),
|
|
331
|
+
referencedControls: referenceInputs.flatMap((input) => accountTokenOf(input, context) ?? []).filter(({ kind }) => kind === "control")
|
|
332
|
+
};
|
|
333
|
+
const reading = readAccountTransaction(body, tokens, context);
|
|
334
|
+
const namedLogics = namedLogicsOf(tokens, body, context);
|
|
335
|
+
const sponsorOutputs = body.outputs.flatMap((output, outputIndex) => output.address === context.sponsorAddress ? [{
|
|
336
|
+
output,
|
|
337
|
+
outputIndex
|
|
338
|
+
}] : []);
|
|
339
|
+
return {
|
|
340
|
+
transaction,
|
|
341
|
+
body,
|
|
342
|
+
witness: transaction.witnessSet().toCore(),
|
|
343
|
+
inputs,
|
|
344
|
+
referenceInputs,
|
|
345
|
+
sponsorInputs,
|
|
346
|
+
sponsorOutputs,
|
|
347
|
+
foreignOutputs: body.outputs.filter((output) => output.address !== context.sponsorAddress && !isAccountOutput(output, context)),
|
|
348
|
+
...statedOf(transaction),
|
|
349
|
+
reading,
|
|
350
|
+
stakeScriptHashes: stakeScriptHashesOf(tokens, reading),
|
|
351
|
+
namedLogics,
|
|
352
|
+
logicHashes: new Set(namedLogics.flatMap(({ logic }) => logic ?? [])),
|
|
353
|
+
sponsorKeys
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
const wellFormed = ({ transaction, body }) => {
|
|
357
|
+
const size = transaction.toCbor().length / 2;
|
|
358
|
+
if (size > MAX_TRANSACTION_BYTES) return {
|
|
359
|
+
rule: "well_formed",
|
|
360
|
+
reason: "too-large",
|
|
361
|
+
size,
|
|
362
|
+
limit: MAX_TRANSACTION_BYTES
|
|
363
|
+
};
|
|
364
|
+
if ((body.proposalProcedures ?? []).length > 0) return {
|
|
365
|
+
rule: "well_formed",
|
|
366
|
+
reason: "proposal-procedures"
|
|
367
|
+
};
|
|
368
|
+
return body.donation === void 0 ? void 0 : {
|
|
369
|
+
rule: "well_formed",
|
|
370
|
+
reason: "treasury-donation"
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Fee mode spends the leased fee UTxO and no other sponsor UTxO; collateral
|
|
375
|
+
* mode spends none. An input whose payment credential cannot be read
|
|
376
|
+
* cannot be told from the sponsor's.
|
|
377
|
+
*/
|
|
378
|
+
const sponsorInputsRule = ({ inputs, sponsorInputs }, { mode }) => {
|
|
379
|
+
const rule = mode.kind === "fee" ? "uses_leased_fee_input" : "no_sponsor_inputs";
|
|
380
|
+
const feeKey = mode.kind === "fee" ? outpointKey(mode.feeUtxo[0]) : void 0;
|
|
381
|
+
if (mode.kind === "fee" && !inputs.some(({ input }) => outpointKey(input) === feeKey)) return {
|
|
382
|
+
rule: "uses_leased_fee_input",
|
|
383
|
+
reason: "fee-input-missing",
|
|
384
|
+
input: toTxIn(mode.feeUtxo[0])
|
|
385
|
+
};
|
|
386
|
+
const unreadable = inputs.find(({ output }) => output !== void 0 && paymentCredentialOf(output.address) === void 0);
|
|
387
|
+
if (unreadable) return {
|
|
388
|
+
rule,
|
|
389
|
+
reason: "unreadable-input",
|
|
390
|
+
input: unreadable.input
|
|
391
|
+
};
|
|
392
|
+
const extra = sponsorInputs.find(({ input }) => outpointKey(input) !== feeKey);
|
|
393
|
+
return extra ? {
|
|
394
|
+
rule,
|
|
395
|
+
reason: "sponsor-input",
|
|
396
|
+
input: extra.input
|
|
397
|
+
} : void 0;
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* The transaction is not flagged as failing phase two, and its collateral
|
|
401
|
+
* is exactly the shared collateral UTxO, returned plainly to the sponsor,
|
|
402
|
+
* with the total collateral set within what it holds. The ledger also
|
|
403
|
+
* takes collateral of at least the collateral percentage of the fee, so a
|
|
404
|
+
* fee above what the shared collateral covers is refused: the service
|
|
405
|
+
* would refuse the total it needs, and the ledger any less.
|
|
406
|
+
*/
|
|
407
|
+
const sharedCollateral = ({ transaction, body }, context) => {
|
|
408
|
+
const refuse = (reason) => ({
|
|
409
|
+
rule: "uses_shared_collateral",
|
|
410
|
+
reason
|
|
411
|
+
});
|
|
412
|
+
if (!transaction.isValid()) return refuse("phase-two-invalid");
|
|
413
|
+
const collaterals = body.collaterals ?? [];
|
|
414
|
+
if (collaterals.length !== 1 || outpointKey(collaterals[0]) !== outpointKey(context.collateralUtxo[0])) return refuse("collateral-inputs");
|
|
415
|
+
const { collateralReturn, totalCollateral } = body;
|
|
416
|
+
if (!collateralReturn) return refuse("no-collateral-return");
|
|
417
|
+
if (collateralReturn.address !== context.sponsorAddress) return refuse("collateral-return-address");
|
|
418
|
+
if (collateralReturn.datum !== void 0 || collateralReturn.datumHash !== void 0) return refuse("collateral-return-datum");
|
|
419
|
+
if (collateralReturn.scriptReference !== void 0) return refuse("collateral-return-reference-script");
|
|
420
|
+
if (totalCollateral === void 0) return refuse("no-total-collateral");
|
|
421
|
+
const available = context.collateralUtxo[1].value.coins;
|
|
422
|
+
if (totalCollateral > available) return {
|
|
423
|
+
rule: "uses_shared_collateral",
|
|
424
|
+
reason: "total-collateral-exceeds",
|
|
425
|
+
totalCollateral,
|
|
426
|
+
available
|
|
427
|
+
};
|
|
428
|
+
const percentage = BigInt(context.collateralPercentage);
|
|
429
|
+
return body.fee * percentage > available * 100n ? {
|
|
430
|
+
rule: "uses_shared_collateral",
|
|
431
|
+
reason: "fee-exceeds-collateral",
|
|
432
|
+
fee: body.fee,
|
|
433
|
+
limit: available * 100n / percentage
|
|
434
|
+
} : void 0;
|
|
435
|
+
};
|
|
436
|
+
const boundedValidity = ({ body }, context) => {
|
|
437
|
+
const upperBound = body.validityInterval?.invalidHereafter;
|
|
438
|
+
if (upperBound === void 0) return {
|
|
439
|
+
rule: "bounded_validity",
|
|
440
|
+
reason: "no-upper-bound"
|
|
441
|
+
};
|
|
442
|
+
if (upperBound > context.latestValiditySlot) return {
|
|
443
|
+
rule: "bounded_validity",
|
|
444
|
+
reason: "upper-bound-too-late",
|
|
445
|
+
upperBound,
|
|
446
|
+
latest: context.latestValiditySlot
|
|
447
|
+
};
|
|
448
|
+
return upperBound <= context.currentSlot ? {
|
|
449
|
+
rule: "bounded_validity",
|
|
450
|
+
reason: "upper-bound-not-future",
|
|
451
|
+
upperBound,
|
|
452
|
+
current: context.currentSlot
|
|
453
|
+
} : void 0;
|
|
454
|
+
};
|
|
455
|
+
const accountTransaction = ({ reading }) => reading.kind === "refused" ? reading.violation : void 0;
|
|
456
|
+
/**
|
|
457
|
+
* Every logic the transaction names is one the service knows. The proxy
|
|
458
|
+
* runs whatever registered script a control datum names, so the service
|
|
459
|
+
* pays for and lends collateral to no account under rules it has not read,
|
|
460
|
+
* and refuses a control datum that names no script hash at all.
|
|
461
|
+
*/
|
|
462
|
+
const knownLogic = ({ namedLogics }, { contractVersion }) => {
|
|
463
|
+
const known = new Set(contractVersion.logics.map(({ hash }) => hash));
|
|
464
|
+
for (const { logic, control, stakeScriptHash } of namedLogics) {
|
|
465
|
+
if (logic === void 0) return {
|
|
466
|
+
rule: "known_logic",
|
|
467
|
+
reason: "logic-missing",
|
|
468
|
+
control,
|
|
469
|
+
stakeScriptHash
|
|
470
|
+
};
|
|
471
|
+
if (!known.has(logic)) return {
|
|
472
|
+
rule: "known_logic",
|
|
473
|
+
reason: "unknown-logic",
|
|
474
|
+
control,
|
|
475
|
+
logic,
|
|
476
|
+
stakeScriptHash
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* The leased fee UTxO pays for an account creation and nothing else: an
|
|
482
|
+
* operation on an existing account pays its own fee and takes the
|
|
483
|
+
* collateral route, whatever it draws. The fee stays within the service's
|
|
484
|
+
* limit, and the fee UTxO is drawn down by exactly the fee, the
|
|
485
|
+
* registration deposit and the control output, within the sponsored limit,
|
|
486
|
+
* and returns as one plain change output the pool can spend.
|
|
487
|
+
*/
|
|
488
|
+
const sponsorOutflowBounded = ({ body, sponsorOutputs, reading }, mode) => {
|
|
489
|
+
const rule = "sponsor_outflow_bounded";
|
|
490
|
+
if (reading.kind !== "creation") return {
|
|
491
|
+
rule,
|
|
492
|
+
reason: "not-creation"
|
|
493
|
+
};
|
|
494
|
+
if (body.fee > MAX_FEE_LOVELACE) return {
|
|
495
|
+
rule,
|
|
496
|
+
reason: "fee-exceeds-limit",
|
|
497
|
+
fee: body.fee,
|
|
498
|
+
limit: MAX_FEE_LOVELACE
|
|
499
|
+
};
|
|
500
|
+
for (const { output, outputIndex } of sponsorOutputs) {
|
|
501
|
+
if (output.datum !== void 0 || output.datumHash !== void 0) return {
|
|
502
|
+
rule,
|
|
503
|
+
reason: "sponsor-output-datum",
|
|
504
|
+
outputIndex
|
|
505
|
+
};
|
|
506
|
+
if (output.scriptReference !== void 0) return {
|
|
507
|
+
rule,
|
|
508
|
+
reason: "sponsor-output-reference-script",
|
|
509
|
+
outputIndex
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (sponsorOutputs.length !== 1) return {
|
|
513
|
+
rule,
|
|
514
|
+
reason: "sponsor-output-count",
|
|
515
|
+
count: sponsorOutputs.length
|
|
516
|
+
};
|
|
517
|
+
const sponsored = mode.feeUtxo[1].value.coins - sponsorOutputs[0].output.value.coins;
|
|
518
|
+
const { deposit, controlLovelace } = reading.creation;
|
|
519
|
+
const expected = body.fee + deposit + controlLovelace;
|
|
520
|
+
if (sponsored !== expected) return {
|
|
521
|
+
rule,
|
|
522
|
+
reason: "draw-mismatch",
|
|
523
|
+
sponsored,
|
|
524
|
+
expected
|
|
525
|
+
};
|
|
526
|
+
return sponsored > mode.maxSponsoredLovelace ? {
|
|
527
|
+
rule,
|
|
528
|
+
reason: "draw-exceeds-limit",
|
|
529
|
+
sponsored,
|
|
530
|
+
limit: mode.maxSponsoredLovelace
|
|
531
|
+
} : void 0;
|
|
532
|
+
};
|
|
533
|
+
/** In collateral mode nothing pays the sponsor key and nothing draws from its reward account. */
|
|
534
|
+
const sponsorOutflowZero = ({ body, withdrawals, sponsorKeys }) => {
|
|
535
|
+
const outputIndex = body.outputs.findIndex((output) => sponsorKeys.paymentKeyHash !== void 0 && keyHashOf(paymentCredentialOf(output.address)) === sponsorKeys.paymentKeyHash);
|
|
536
|
+
if (outputIndex >= 0) return {
|
|
537
|
+
rule: "sponsor_outflow_zero",
|
|
538
|
+
reason: "pays-sponsor",
|
|
539
|
+
outputIndex
|
|
540
|
+
};
|
|
541
|
+
const withdrawal = withdrawals.find(({ stakeAddress }) => sponsorKeys.stakeKeyHash !== void 0 && keyHashOf(stakeCredentialOf(stakeAddress)) === sponsorKeys.stakeKeyHash);
|
|
542
|
+
return withdrawal ? {
|
|
543
|
+
rule: "sponsor_outflow_zero",
|
|
544
|
+
reason: "sponsor-withdrawal",
|
|
545
|
+
rewardAccount: withdrawal.stakeAddress
|
|
546
|
+
} : void 0;
|
|
547
|
+
};
|
|
548
|
+
const sponsorOutflow = (analysis, { mode }) => mode.kind === "fee" ? sponsorOutflowBounded(analysis, mode) : sponsorOutflowZero(analysis);
|
|
549
|
+
/** Outputs away from the sponsor and the account are covered by what non-sponsor inputs and withdrawals bring. */
|
|
550
|
+
const noSponsorValueElsewhere = ({ withdrawals, inputs, sponsorInputs, foreignOutputs, sponsorKeys }) => {
|
|
551
|
+
const supply = balanceOf(inputs.filter((input) => !sponsorInputs.includes(input)).flatMap(({ output }) => output ? [output.value] : []));
|
|
552
|
+
for (const { stakeAddress, quantity } of withdrawals) if (!(sponsorKeys.stakeKeyHash !== void 0 && keyHashOf(stakeCredentialOf(stakeAddress)) === sponsorKeys.stakeKeyHash)) supply.set(LOVELACE_KEY, (supply.get(LOVELACE_KEY) ?? 0n) + quantity);
|
|
553
|
+
const demand = balanceOf(foreignOutputs.map((output) => output.value));
|
|
554
|
+
for (const [key, demanded] of demand) {
|
|
555
|
+
const supplied = supply.get(key) ?? 0n;
|
|
556
|
+
if (demanded > supplied) return {
|
|
557
|
+
rule: "no_sponsor_value_elsewhere",
|
|
558
|
+
reason: "uncovered-outflow",
|
|
559
|
+
demanded,
|
|
560
|
+
supplied,
|
|
561
|
+
...key === LOVELACE_KEY ? {} : { assetId: key }
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Lace's own rule on reference inputs: one carrying a script must be a
|
|
567
|
+
* version's parked script UTxO on the network, carrying exactly the script
|
|
568
|
+
* parked there, and a parked script's UTxO must carry its script.
|
|
569
|
+
*/
|
|
570
|
+
const foreignReferenceScriptOf = (referenceInputs, { contractVersion, networkMagic }) => {
|
|
571
|
+
const parked = new Map((contractVersion.referenceScripts[networkMagic] ?? []).map(({ txIn, scriptHash }) => [outpointKey(txIn), scriptHash]));
|
|
572
|
+
for (const { input, output } of referenceInputs) {
|
|
573
|
+
const script = output?.scriptReference;
|
|
574
|
+
const scriptHash = script && _laceio_cardano_core.Serialization.Script.fromCore(script).hash();
|
|
575
|
+
const expected = parked.get(outpointKey(input));
|
|
576
|
+
if (expected === void 0 ? scriptHash !== void 0 : output !== void 0 && scriptHash !== expected) return {
|
|
577
|
+
rule: "no_foreign_scripts",
|
|
578
|
+
reason: "foreign-reference-script",
|
|
579
|
+
input,
|
|
580
|
+
...scriptHash && { scriptHash }
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
/**
|
|
585
|
+
* Every script the transaction runs or attaches belongs to the account.
|
|
586
|
+
* Inputs and mints use the account proxy alone. A certificate or a vote may
|
|
587
|
+
* name the proxy or the stake script of an account the transaction belongs
|
|
588
|
+
* to. A logic its control datums name may also run through a withdrawal,
|
|
589
|
+
* of zero only, and be attached. No reward account is drawn from twice.
|
|
590
|
+
*/
|
|
591
|
+
const noForeignScripts = ({ body, witness, inputs, referenceInputs, withdrawals, stakeScriptHashes, logicHashes }, context) => {
|
|
592
|
+
const rule = "no_foreign_scripts";
|
|
593
|
+
const proxy = context.contractVersion.accountValidatorHash;
|
|
594
|
+
const accountScripts = new Set([proxy, ...stakeScriptHashes]);
|
|
595
|
+
const runnable = new Set([...accountScripts, ...logicHashes]);
|
|
596
|
+
for (const { input, output } of inputs) {
|
|
597
|
+
const scriptHash = output && scriptHashOf$1(paymentCredentialOf(output.address));
|
|
598
|
+
if (scriptHash !== void 0 && scriptHash !== proxy) return {
|
|
599
|
+
rule,
|
|
600
|
+
reason: "foreign-script-input",
|
|
601
|
+
input,
|
|
602
|
+
scriptHash
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
for (const assetId of body.mint?.keys() ?? []) {
|
|
606
|
+
const policyId = policyIdOf(assetId);
|
|
607
|
+
if (policyId !== proxy) return {
|
|
608
|
+
rule,
|
|
609
|
+
reason: "foreign-mint",
|
|
610
|
+
policyId
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
for (const [certificateIndex, certificate] of (body.certificates ?? []).entries()) {
|
|
614
|
+
const scriptHash = certificateCredentials(certificate).map(scriptHashOf$1).find((hash) => hash !== void 0 && !accountScripts.has(hash));
|
|
615
|
+
if (scriptHash) return {
|
|
616
|
+
rule,
|
|
617
|
+
reason: "foreign-certificate",
|
|
618
|
+
certificateIndex,
|
|
619
|
+
scriptHash
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const drawn = /* @__PURE__ */ new Set();
|
|
623
|
+
for (const { stakeAddress: rewardAccount, quantity } of withdrawals) {
|
|
624
|
+
if (drawn.has(rewardAccount)) return {
|
|
625
|
+
rule,
|
|
626
|
+
reason: "duplicate-withdrawal",
|
|
627
|
+
rewardAccount
|
|
628
|
+
};
|
|
629
|
+
drawn.add(rewardAccount);
|
|
630
|
+
const scriptHash = scriptHashOf$1(stakeCredentialOf(rewardAccount));
|
|
631
|
+
if (scriptHash !== void 0 && !runnable.has(scriptHash)) return {
|
|
632
|
+
rule,
|
|
633
|
+
reason: "foreign-withdrawal",
|
|
634
|
+
rewardAccount
|
|
635
|
+
};
|
|
636
|
+
if (scriptHash !== void 0 && logicHashes.has(scriptHash) && quantity !== 0n) return {
|
|
637
|
+
rule,
|
|
638
|
+
reason: "logic-draw",
|
|
639
|
+
rewardAccount,
|
|
640
|
+
logic: scriptHash,
|
|
641
|
+
quantity
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
for (const { voter } of body.votingProcedures ?? []) {
|
|
645
|
+
const scriptHash = scriptHashOf$1(voter.credential);
|
|
646
|
+
if (scriptHash !== void 0 && !accountScripts.has(scriptHash)) return {
|
|
647
|
+
rule,
|
|
648
|
+
reason: "foreign-voter",
|
|
649
|
+
scriptHash
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
const scripts = witness.scripts ?? [];
|
|
653
|
+
if (scripts.some((script) => _laceio_cardano_core.Cardano.isNativeScript(script))) return {
|
|
654
|
+
rule,
|
|
655
|
+
reason: "native-script"
|
|
656
|
+
};
|
|
657
|
+
for (const script of scripts) {
|
|
658
|
+
const scriptHash = _laceio_cardano_core.Serialization.Script.fromCore(script).hash();
|
|
659
|
+
if (!runnable.has(scriptHash)) return {
|
|
660
|
+
rule,
|
|
661
|
+
reason: "foreign-attached-script",
|
|
662
|
+
scriptHash
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return foreignReferenceScriptOf(referenceInputs, context);
|
|
666
|
+
};
|
|
667
|
+
/**
|
|
668
|
+
* The Plutus languages of the scripts the transaction attaches, then of the
|
|
669
|
+
* reference scripts its inputs and reference inputs carry: the service's
|
|
670
|
+
* language view covers all of them, not only the scripts that run.
|
|
671
|
+
*/
|
|
672
|
+
const languagesOf = ({ witness, inputs, referenceInputs }) => [...(witness.scripts ?? []).flatMap((script) => _laceio_cardano_core.Cardano.isPlutusScript(script) ? [script.version] : []), ...[...inputs, ...referenceInputs].flatMap(({ output }) => output?.scriptReference && _laceio_cardano_core.Cardano.isPlutusScript(output.scriptReference) ? [output.scriptReference.version] : [])];
|
|
673
|
+
/** The redeemers that stand in for a witness set carrying datums and no redeemers. */
|
|
674
|
+
const EMPTY_REDEEMERS = "a0";
|
|
675
|
+
/**
|
|
676
|
+
* The script data hash the body commits to is the one the witness set's
|
|
677
|
+
* redeemers and datums hash to, under the cost models, with the language
|
|
678
|
+
* view of every attached and referenced script. Every script an account
|
|
679
|
+
* runs is Plutus V3, so a transaction carrying any other language is
|
|
680
|
+
* refused before a view is formed.
|
|
681
|
+
*/
|
|
682
|
+
const scriptDataHash = (analysis, { costModels }) => {
|
|
683
|
+
const rule = "script_data_hash";
|
|
684
|
+
const declared = analysis.body.scriptIntegrityHash;
|
|
685
|
+
const { redeemers, datums } = analysis.witnessScriptData;
|
|
686
|
+
if (redeemers === void 0 && datums === void 0) return declared === void 0 ? void 0 : {
|
|
687
|
+
rule,
|
|
688
|
+
reason: "unexpected-script-data-hash",
|
|
689
|
+
declared
|
|
690
|
+
};
|
|
691
|
+
if (declared === void 0) return {
|
|
692
|
+
rule,
|
|
693
|
+
reason: "missing-script-data-hash"
|
|
694
|
+
};
|
|
695
|
+
const languages = languagesOf(analysis);
|
|
696
|
+
const foreign = languages.find((language) => language !== _laceio_cardano_core.Cardano.PlutusLanguageVersion.V3);
|
|
697
|
+
if (foreign !== void 0) return {
|
|
698
|
+
rule,
|
|
699
|
+
reason: "non-v3-script",
|
|
700
|
+
language: foreign
|
|
701
|
+
};
|
|
702
|
+
const views = new _laceio_cardano_core.Serialization.Costmdls();
|
|
703
|
+
for (const language of new Set(languages)) {
|
|
704
|
+
const costs = costModels.get(language);
|
|
705
|
+
if (!costs) return {
|
|
706
|
+
rule,
|
|
707
|
+
reason: "missing-cost-model",
|
|
708
|
+
language
|
|
709
|
+
};
|
|
710
|
+
views.insert(new _laceio_cardano_core.Serialization.CostModel(language, costs));
|
|
711
|
+
}
|
|
712
|
+
const computed = _laceio_cardano_crypto.blake2b.hash((0, _laceio_cardano_util.HexBlob)(`${redeemers ?? EMPTY_REDEEMERS}${datums ?? ""}${views.languageViewsEncoding()}`), 32);
|
|
713
|
+
return computed === declared ? void 0 : {
|
|
714
|
+
rule,
|
|
715
|
+
reason: "script-data-hash-mismatch",
|
|
716
|
+
declared,
|
|
717
|
+
computed
|
|
718
|
+
};
|
|
719
|
+
};
|
|
720
|
+
/** Neither sponsor key signs for anything but the sponsor's own inputs. */
|
|
721
|
+
const signers = ({ body, withdrawals, sponsorKeys }) => {
|
|
722
|
+
const keyHash = (body.requiredExtraSignatures ?? []).find((signer) => signer === sponsorKeys.paymentKeyHash || signer === sponsorKeys.stakeKeyHash);
|
|
723
|
+
if (keyHash) return {
|
|
724
|
+
rule: "signers",
|
|
725
|
+
reason: "sponsor-required-signer",
|
|
726
|
+
keyHash
|
|
727
|
+
};
|
|
728
|
+
const withdrawal = withdrawals.find(({ stakeAddress }) => {
|
|
729
|
+
const credential = stakeCredentialOf(stakeAddress);
|
|
730
|
+
return credential !== void 0 && namesSponsorKey(credential, sponsorKeys);
|
|
731
|
+
});
|
|
732
|
+
if (withdrawal) return {
|
|
733
|
+
rule: "signers",
|
|
734
|
+
reason: "sponsor-withdrawal",
|
|
735
|
+
rewardAccount: withdrawal.stakeAddress
|
|
736
|
+
};
|
|
737
|
+
const certificateIndex = (body.certificates ?? []).findIndex((certificate) => certificateCredentials(certificate).some((credential) => namesSponsorKey(credential, sponsorKeys)));
|
|
738
|
+
if (certificateIndex >= 0) return {
|
|
739
|
+
rule: "signers",
|
|
740
|
+
reason: "sponsor-certificate",
|
|
741
|
+
certificateIndex
|
|
742
|
+
};
|
|
743
|
+
return (body.votingProcedures ?? []).some(({ voter }) => namesSponsorKey(voter.credential, sponsorKeys)) ? {
|
|
744
|
+
rule: "signers",
|
|
745
|
+
reason: "sponsor-voter"
|
|
746
|
+
} : void 0;
|
|
747
|
+
};
|
|
748
|
+
/** The service's rules in its check order; it evaluates the scripts between the last two. */
|
|
749
|
+
const RULES = [
|
|
750
|
+
wellFormed,
|
|
751
|
+
sponsorInputsRule,
|
|
752
|
+
sharedCollateral,
|
|
753
|
+
boundedValidity,
|
|
754
|
+
accountTransaction,
|
|
755
|
+
knownLogic,
|
|
756
|
+
sponsorOutflow,
|
|
757
|
+
noSponsorValueElsewhere,
|
|
758
|
+
noForeignScripts,
|
|
759
|
+
scriptDataHash,
|
|
760
|
+
signers
|
|
761
|
+
];
|
|
762
|
+
/**
|
|
763
|
+
* Every rule of the fee sponsor service's policy that a transaction breaks,
|
|
764
|
+
* at most one per rule and in the service's check order, so the first is
|
|
765
|
+
* the one the service would answer with. Lace checks its custody builds
|
|
766
|
+
* with it before asking the sponsor for a witness, so a refusal from the
|
|
767
|
+
* service points at a real defect rather than a build Lace knew to be wrong.
|
|
768
|
+
*
|
|
769
|
+
* The script evaluation rule is not mirrored: it needs the chain. Of the
|
|
770
|
+
* sponsor's UTxOs, only the leased fee UTxO, the shared collateral and
|
|
771
|
+
* outputs locked by the sponsor payment key are recognised; the service
|
|
772
|
+
* also knows the rest of its pool. The service knows the logics its
|
|
773
|
+
* operator lists, which Lace takes to be the contract version's. Beyond the
|
|
774
|
+
* service's rules, the shared collateral rule refuses a fee above what the
|
|
775
|
+
* shared collateral covers, which the ledger would refuse after the service
|
|
776
|
+
* signed, and the script rule refuses a reference script other than the
|
|
777
|
+
* version's parked scripts on the network.
|
|
778
|
+
*
|
|
779
|
+
* @param resolvedInputs - the outputs the transaction's inputs spend and its
|
|
780
|
+
* reference inputs name; an input or a reference input without one is
|
|
781
|
+
* read as the service reads one the chain does not know, which is never
|
|
782
|
+
* an account's control or grant UTxO and carries no reference script
|
|
783
|
+
*/
|
|
784
|
+
const findCustodySponsorRuleViolations = (transaction, resolvedInputs, context) => {
|
|
785
|
+
const analysis = analyse(transaction, resolvedInputs, context);
|
|
786
|
+
return RULES.flatMap((rule) => {
|
|
787
|
+
const violation = rule(analysis, context);
|
|
788
|
+
return violation ? [violation] : [];
|
|
789
|
+
});
|
|
790
|
+
};
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region ../../packages/lib/cardano-custody/src/state/transitions.ts
|
|
793
|
+
/**
|
|
794
|
+
* The state Lace creates an account with: `logic` as its rules, the creator
|
|
795
|
+
* as the only device, generation zero and zero counters. The logic accepts
|
|
796
|
+
* any generation at creation; consent refuses every other initial state.
|
|
797
|
+
*
|
|
798
|
+
* @param logic - the applied hash of the logic the account runs, normally the
|
|
799
|
+
* contract version's `currentLogicHash`
|
|
800
|
+
*/
|
|
801
|
+
const createInitialCustodyAccountState = (creatorKeyHash, logic) => {
|
|
802
|
+
if (!require_hex.isHash28Hex(logic)) return require_hex.fail({
|
|
803
|
+
code: "invalid-logic-hash",
|
|
804
|
+
logic
|
|
805
|
+
});
|
|
806
|
+
return require_hex.isHash28Hex(creatorKeyHash) ? require_hex.ok({
|
|
807
|
+
logic,
|
|
808
|
+
devices: [creatorKeyHash],
|
|
809
|
+
grantGeneration: 0n,
|
|
810
|
+
nextSlot: 0n,
|
|
811
|
+
revoked: [],
|
|
812
|
+
outstanding: 0n
|
|
813
|
+
}) : require_hex.fail({
|
|
814
|
+
code: "invalid-device-key-hash",
|
|
815
|
+
device: creatorKeyHash
|
|
816
|
+
});
|
|
817
|
+
};
|
|
818
|
+
/** Checks a grant Lace is about to write: authoring rules, expiry, separation. */
|
|
819
|
+
const checkAuthoredGrant = (state, grant, now) => {
|
|
820
|
+
const violations = require_outputs.findCustodyGrantViolations(grant);
|
|
821
|
+
if (violations.length > 0) return {
|
|
822
|
+
code: "invalid-grant",
|
|
823
|
+
violations
|
|
824
|
+
};
|
|
825
|
+
if (!Number.isFinite(now)) return {
|
|
826
|
+
code: "invalid-now",
|
|
827
|
+
now
|
|
828
|
+
};
|
|
829
|
+
if (require_outputs.isCustodyGrantExpired(grant, now)) return {
|
|
830
|
+
code: "grant-expired",
|
|
831
|
+
slot: grant.slot,
|
|
832
|
+
expiresAt: grant.scope.expiresAt
|
|
833
|
+
};
|
|
834
|
+
if (state.devices.includes(grant.grantee)) return {
|
|
835
|
+
code: "grantee-is-device",
|
|
836
|
+
grantee: grant.grantee
|
|
837
|
+
};
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* Refuses a grantee that still holds a grant it could spend. One grant per
|
|
841
|
+
* agent key keeps each agent's authority in one place, so a revocation or a
|
|
842
|
+
* replacement always covers all of it. A grant judged against an older
|
|
843
|
+
* state than its own counts too: it is in flight and about to be current.
|
|
844
|
+
*/
|
|
845
|
+
const checkGranteeHoldsNoCurrentGrant = (state, grants, { grantee, now }) => {
|
|
846
|
+
const held = grants.find((grant) => {
|
|
847
|
+
if (grant.grantee !== grantee) return false;
|
|
848
|
+
const liveness = require_grant_status.custodyGrantLiveness(grant, state, now);
|
|
849
|
+
return liveness === "current" || liveness === "future-generation";
|
|
850
|
+
});
|
|
851
|
+
return held ? {
|
|
852
|
+
code: "grantee-has-grant",
|
|
853
|
+
grantee,
|
|
854
|
+
slot: held.slot
|
|
855
|
+
} : void 0;
|
|
856
|
+
};
|
|
857
|
+
/**
|
|
858
|
+
* Issues one grant in the state's next slot under its current generation:
|
|
859
|
+
* the next slot and the outstanding count move by one, and the result
|
|
860
|
+
* carries the datum of the new grant UTxO.
|
|
861
|
+
*
|
|
862
|
+
* @param grants - every grant of the account, from its grant UTxOs and from
|
|
863
|
+
* issues still in flight, to keep one current grant per agent key
|
|
864
|
+
*/
|
|
865
|
+
const issueCustodyGrant = (state, { grantee, scope, now }, grants) => {
|
|
866
|
+
if (state.nextSlot >= require_tokens.CUSTODY_SLOT_LIMIT) return require_hex.fail({
|
|
867
|
+
code: "slot-space-exhausted",
|
|
868
|
+
nextSlot: state.nextSlot
|
|
869
|
+
});
|
|
870
|
+
const grant = {
|
|
871
|
+
slot: state.nextSlot,
|
|
872
|
+
grantee,
|
|
873
|
+
generation: state.grantGeneration,
|
|
874
|
+
scope
|
|
875
|
+
};
|
|
876
|
+
const error = checkAuthoredGrant(state, grant, now) ?? checkGranteeHoldsNoCurrentGrant(state, grants, {
|
|
877
|
+
grantee,
|
|
878
|
+
now
|
|
879
|
+
});
|
|
880
|
+
if (error) return require_hex.fail(error);
|
|
881
|
+
if (state.outstanding >= BigInt(16)) return require_hex.fail({
|
|
882
|
+
code: "grant-limit",
|
|
883
|
+
limit: 16
|
|
884
|
+
});
|
|
885
|
+
return require_hex.ok({
|
|
886
|
+
state: {
|
|
887
|
+
...state,
|
|
888
|
+
nextSlot: state.nextSlot + 1n,
|
|
889
|
+
outstanding: state.outstanding + 1n
|
|
890
|
+
},
|
|
891
|
+
grant
|
|
892
|
+
});
|
|
893
|
+
};
|
|
894
|
+
/**
|
|
895
|
+
* Revokes the current grant in `slot` by appending the slot to the revoked
|
|
896
|
+
* list. The grant UTxO stays until a sweep burns its token.
|
|
897
|
+
*
|
|
898
|
+
* @param grants - every grant of the account; the one in `slot` must be current
|
|
899
|
+
*/
|
|
900
|
+
const revokeCustodyGrant = (state, slot, grants) => {
|
|
901
|
+
const grant = grants.find((candidate) => candidate.slot === slot);
|
|
902
|
+
if (!grant) return require_hex.fail({
|
|
903
|
+
code: "grant-not-found",
|
|
904
|
+
slot
|
|
905
|
+
});
|
|
906
|
+
if (!require_outputs.isCustodyGrantCurrent(grant, state)) return require_hex.fail({
|
|
907
|
+
code: "grant-not-current",
|
|
908
|
+
slot,
|
|
909
|
+
liveness: require_grant_status.custodyGrantLiveness(grant, state, 0)
|
|
910
|
+
});
|
|
911
|
+
if (state.revoked.length >= 32) return require_hex.fail({
|
|
912
|
+
code: "revoked-list-full",
|
|
913
|
+
limit: 32
|
|
914
|
+
});
|
|
915
|
+
return require_hex.ok({
|
|
916
|
+
...state,
|
|
917
|
+
revoked: [...state.revoked, slot]
|
|
918
|
+
});
|
|
919
|
+
};
|
|
920
|
+
/**
|
|
921
|
+
* Revokes the current grant in `request.slot` and issues its replacement in
|
|
922
|
+
* the next slot, in one state. Both happen in one transaction; the old
|
|
923
|
+
* grant UTxO stays until a later sweep.
|
|
924
|
+
*
|
|
925
|
+
* @param grants - every grant of the account, as for `issueCustodyGrant`
|
|
926
|
+
*/
|
|
927
|
+
const replaceCustodyGrant = (state, { slot, ...request }, grants) => {
|
|
928
|
+
const revoked = revokeCustodyGrant(state, slot, grants);
|
|
929
|
+
return revoked.ok ? issueCustodyGrant(revoked.value, request, grants) : revoked;
|
|
930
|
+
};
|
|
931
|
+
/** Kills every grant: the generation moves on and the revoked list clears. */
|
|
932
|
+
const revokeAllCustodyGrants = (state) => ({
|
|
933
|
+
...state,
|
|
934
|
+
grantGeneration: state.grantGeneration + 1n,
|
|
935
|
+
revoked: []
|
|
936
|
+
});
|
|
937
|
+
/**
|
|
938
|
+
* Sweeps dead grants: their tokens burn, the outstanding count drops, and
|
|
939
|
+
* their slots leave the revoked list. Pruning is safe because slots are never
|
|
940
|
+
* reissued, and it keeps the list from filling up. Each grant must be dead
|
|
941
|
+
* against `state`, the state before the sweep.
|
|
942
|
+
*
|
|
943
|
+
* @param grants - the grants to sweep, at most `CUSTODY_MAX_GRANT_BATCH`
|
|
944
|
+
* @param now - POSIX milliseconds; a grant dead by expiry alone must have
|
|
945
|
+
* expired strictly before it, so a validity interval can start after the
|
|
946
|
+
* expiry
|
|
947
|
+
*/
|
|
948
|
+
const sweepCustodyGrants = (state, grants, now) => {
|
|
949
|
+
if (!Number.isFinite(now)) return require_hex.fail({
|
|
950
|
+
code: "invalid-now",
|
|
951
|
+
now
|
|
952
|
+
});
|
|
953
|
+
if (grants.length === 0) return require_hex.fail({ code: "sweep-empty" });
|
|
954
|
+
if (grants.length > 8) return require_hex.fail({
|
|
955
|
+
code: "sweep-batch-limit",
|
|
956
|
+
count: grants.length,
|
|
957
|
+
limit: 8
|
|
958
|
+
});
|
|
959
|
+
const slots = /* @__PURE__ */ new Set();
|
|
960
|
+
let startsAfter;
|
|
961
|
+
for (const grant of grants) {
|
|
962
|
+
if (slots.has(grant.slot)) return require_hex.fail({
|
|
963
|
+
code: "duplicate-grant-slot",
|
|
964
|
+
slot: grant.slot
|
|
965
|
+
});
|
|
966
|
+
slots.add(grant.slot);
|
|
967
|
+
const liveness = require_grant_status.custodyGrantLiveness(grant, state, now);
|
|
968
|
+
const { expiresAt } = grant.scope;
|
|
969
|
+
if (!(liveness === "superseded" || liveness === "revoked" || liveness === "expired" && expiresAt < BigInt(Math.floor(now)))) return require_hex.fail({
|
|
970
|
+
code: "grant-not-dead",
|
|
971
|
+
slot: grant.slot,
|
|
972
|
+
liveness
|
|
973
|
+
});
|
|
974
|
+
if (liveness === "expired" && (startsAfter === void 0 || expiresAt > startsAfter)) startsAfter = expiresAt;
|
|
975
|
+
}
|
|
976
|
+
return require_hex.ok({
|
|
977
|
+
state: {
|
|
978
|
+
...state,
|
|
979
|
+
revoked: state.revoked.filter((slot) => !slots.has(slot)),
|
|
980
|
+
outstanding: state.outstanding - BigInt(grants.length)
|
|
981
|
+
},
|
|
982
|
+
...startsAfter !== void 0 && { startsAfter }
|
|
983
|
+
});
|
|
984
|
+
};
|
|
985
|
+
/**
|
|
986
|
+
* Lists a new device key. Every device has full owner authority, so a key
|
|
987
|
+
* that holds a current grant is refused: removing the device later would
|
|
988
|
+
* leave the grant in force.
|
|
989
|
+
*
|
|
990
|
+
* @param grants - every grant of the account
|
|
991
|
+
*/
|
|
992
|
+
const addCustodyDevice = (state, device, grants) => {
|
|
993
|
+
if (!require_hex.isHash28Hex(device)) return require_hex.fail({
|
|
994
|
+
code: "invalid-device-key-hash",
|
|
995
|
+
device
|
|
996
|
+
});
|
|
997
|
+
if (state.devices.includes(device)) return require_hex.fail({
|
|
998
|
+
code: "device-already-listed",
|
|
999
|
+
device
|
|
1000
|
+
});
|
|
1001
|
+
if (state.devices.length >= 8) return require_hex.fail({
|
|
1002
|
+
code: "device-limit",
|
|
1003
|
+
limit: 8
|
|
1004
|
+
});
|
|
1005
|
+
const slots = grants.filter((grant) => grant.grantee === device && require_outputs.isCustodyGrantCurrent(grant, state)).map((grant) => grant.slot);
|
|
1006
|
+
if (slots.length > 0) return require_hex.fail({
|
|
1007
|
+
code: "device-is-grantee",
|
|
1008
|
+
device,
|
|
1009
|
+
slots
|
|
1010
|
+
});
|
|
1011
|
+
return require_hex.ok({
|
|
1012
|
+
...state,
|
|
1013
|
+
devices: [...state.devices, device]
|
|
1014
|
+
});
|
|
1015
|
+
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Removes a device key. Refuses when no well-formed device key would remain,
|
|
1018
|
+
* because the account would then accept no owner operation ever again. Grants
|
|
1019
|
+
* held by the same key stay in force.
|
|
1020
|
+
*/
|
|
1021
|
+
const removeCustodyDevice = (state, device) => {
|
|
1022
|
+
const listed = state.devices.find((existing) => existing === device);
|
|
1023
|
+
if (listed === void 0) return require_hex.fail({
|
|
1024
|
+
code: "device-not-listed",
|
|
1025
|
+
device
|
|
1026
|
+
});
|
|
1027
|
+
const devices = state.devices.filter((existing) => existing !== device);
|
|
1028
|
+
if (!devices.some((remaining) => require_hex.isHash28Hex(remaining))) return require_hex.fail({
|
|
1029
|
+
code: "last-device",
|
|
1030
|
+
device: listed
|
|
1031
|
+
});
|
|
1032
|
+
return require_hex.ok({
|
|
1033
|
+
...state,
|
|
1034
|
+
devices
|
|
1035
|
+
});
|
|
1036
|
+
};
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region ../../packages/contract/cardano-context/src/estimate-signed-tx-size.ts
|
|
1039
|
+
const DUMMY_SIGNATURE = "f".repeat(128);
|
|
1040
|
+
const dummyVkey = (index) => index.toString(16).padStart(64, "0");
|
|
1041
|
+
/**
|
|
1042
|
+
* Byte length of `tx` once signed. Estimated exactly by populating one dummy
|
|
1043
|
+
* vkey witness per unique signer and letting the serializer measure the result
|
|
1044
|
+
* (the cardano-sdk `getTxSize` technique), which counts the real Conway witness
|
|
1045
|
+
* set framing, unlike a hand-summed byte formula.
|
|
1046
|
+
*
|
|
1047
|
+
* `resolvedInputs` must resolve every input so the signer count (input payment
|
|
1048
|
+
* keys plus withdrawal / certificate stake keys) is complete: an incomplete set
|
|
1049
|
+
* would under-count witnesses and under-estimate the size, the dangerous
|
|
1050
|
+
* direction for a size guard.
|
|
1051
|
+
*/
|
|
1052
|
+
const estimateSignedTxSize = (tx, resolvedInputs) => {
|
|
1053
|
+
const core = tx.toCore();
|
|
1054
|
+
const signerCount = require_TransactionBuilder.getUniqueSignerKeyHashes(core, resolvedInputs).size;
|
|
1055
|
+
const signatures = /* @__PURE__ */ new Map();
|
|
1056
|
+
for (let index = 0; index < signerCount; index++) signatures.set(dummyVkey(index), DUMMY_SIGNATURE);
|
|
1057
|
+
return _laceio_cardano_core.Serialization.Transaction.fromCore({
|
|
1058
|
+
...core,
|
|
1059
|
+
witness: {
|
|
1060
|
+
...core.witness,
|
|
1061
|
+
signatures
|
|
1062
|
+
}
|
|
1063
|
+
}).toCbor().length / 2;
|
|
1064
|
+
};
|
|
1065
|
+
//#endregion
|
|
1066
|
+
//#region ../../packages/contract/cardano-context/src/tx-builder/transaction-limits.ts
|
|
1067
|
+
/**
|
|
1068
|
+
* Conway's cap on the reference-script bytes one transaction reads, summed
|
|
1069
|
+
* over its spent and reference inputs. A ledger constant, not a protocol
|
|
1070
|
+
* parameter.
|
|
1071
|
+
*/
|
|
1072
|
+
const MAX_REFERENCE_SCRIPT_SIZE_PER_TX = 204800;
|
|
1073
|
+
/** A built transaction would be refused by the ledger for exceeding a limit. */
|
|
1074
|
+
var TransactionLimitsExceededError = class extends Error {
|
|
1075
|
+
constructor(exceeded, usage, limits) {
|
|
1076
|
+
super(`Transaction exceeds protocol limits (${exceeded.join(", ")}): execution units ${usage.executionUnits.memory}/${limits.maxExecutionUnitsPerTransaction.memory} memory, ${usage.executionUnits.steps}/${limits.maxExecutionUnitsPerTransaction.steps} steps; signed size ${usage.signedTxSize}/${limits.maxTxSize} bytes; reference scripts ${usage.referenceScriptSize}/${MAX_REFERENCE_SCRIPT_SIZE_PER_TX} bytes`);
|
|
1077
|
+
this.exceeded = exceeded;
|
|
1078
|
+
this.usage = usage;
|
|
1079
|
+
this.limits = limits;
|
|
1080
|
+
this.name = "TransactionLimitsExceededError";
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
/**
|
|
1084
|
+
* Checks a built transaction against the per-transaction execution-unit,
|
|
1085
|
+
* size and reference-script limits, which `TransactionBuilder` does not
|
|
1086
|
+
* enforce. Run it on the final (evaluated) transaction. The size is measured
|
|
1087
|
+
* with one vkey witness per required signer, so `resolvedInputs` must resolve
|
|
1088
|
+
* every input and collateral input, as for `estimateSignedTxSize`. It must
|
|
1089
|
+
* also resolve every reference input, whose scripts count toward the
|
|
1090
|
+
* reference-script limit.
|
|
1091
|
+
*
|
|
1092
|
+
* @returns The measured usage when every limit holds.
|
|
1093
|
+
* @throws `TransactionLimitsExceededError` When any limit is exceeded.
|
|
1094
|
+
*/
|
|
1095
|
+
const assertWithinTransactionLimits = (tx, resolvedInputs, limits) => {
|
|
1096
|
+
const redeemers = tx.witnessSet().redeemers()?.toCore() ?? [];
|
|
1097
|
+
const body = tx.body().toCore();
|
|
1098
|
+
const usage = {
|
|
1099
|
+
executionUnits: redeemers.reduce((total, { executionUnits }) => ({
|
|
1100
|
+
memory: total.memory + executionUnits.memory,
|
|
1101
|
+
steps: total.steps + executionUnits.steps
|
|
1102
|
+
}), {
|
|
1103
|
+
memory: 0,
|
|
1104
|
+
steps: 0
|
|
1105
|
+
}),
|
|
1106
|
+
signedTxSize: estimateSignedTxSize(tx, resolvedInputs),
|
|
1107
|
+
referenceScriptSize: require_TransactionBuilder.referenceScriptSize([...body.inputs, ...body.referenceInputs ?? []], resolvedInputs)
|
|
1108
|
+
};
|
|
1109
|
+
const exceeded = [];
|
|
1110
|
+
if (usage.executionUnits.memory > limits.maxExecutionUnitsPerTransaction.memory) exceeded.push("executionMemory");
|
|
1111
|
+
if (usage.executionUnits.steps > limits.maxExecutionUnitsPerTransaction.steps) exceeded.push("executionSteps");
|
|
1112
|
+
if (usage.signedTxSize > limits.maxTxSize) exceeded.push("txSize");
|
|
1113
|
+
if (usage.referenceScriptSize > MAX_REFERENCE_SCRIPT_SIZE_PER_TX) exceeded.push("referenceScriptSize");
|
|
1114
|
+
if (exceeded.length > 0) throw new TransactionLimitsExceededError(exceeded, usage, limits);
|
|
1115
|
+
return usage;
|
|
1116
|
+
};
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/build-failure.ts
|
|
1119
|
+
/**
|
|
1120
|
+
* Stops a custody build with a typed reason. The build side effect reports
|
|
1121
|
+
* it as the failure; any other error thrown during a build is a defect and
|
|
1122
|
+
* is reported as `build-failed`.
|
|
1123
|
+
*/
|
|
1124
|
+
var CustodyBuildFailure = class extends Error {
|
|
1125
|
+
constructor(reason) {
|
|
1126
|
+
super(`Custody transaction build failed: ${reason.code}`);
|
|
1127
|
+
this.reason = reason;
|
|
1128
|
+
this.name = "CustodyBuildFailure";
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
const failCustodyBuild = (reason) => {
|
|
1132
|
+
throw new CustodyBuildFailure(reason);
|
|
1133
|
+
};
|
|
1134
|
+
/**
|
|
1135
|
+
* The build failure a state transition's refusal stands for. A full account
|
|
1136
|
+
* the owner can act on has its own code: sweep dead grants to free the
|
|
1137
|
+
* outstanding count or the revoked list, or wait for a grant to die.
|
|
1138
|
+
*/
|
|
1139
|
+
const transitionFailureOf = (error) => {
|
|
1140
|
+
if (error.code === "grant-limit" || error.code === "revoked-list-full") return {
|
|
1141
|
+
code: error.code,
|
|
1142
|
+
limit: error.limit
|
|
1143
|
+
};
|
|
1144
|
+
if (error.code === "grant-not-dead") return {
|
|
1145
|
+
code: "sweep-not-dead",
|
|
1146
|
+
slot: error.slot,
|
|
1147
|
+
liveness: error.liveness
|
|
1148
|
+
};
|
|
1149
|
+
return {
|
|
1150
|
+
code: "invalid-operation",
|
|
1151
|
+
reason: "state-transition",
|
|
1152
|
+
error
|
|
1153
|
+
};
|
|
1154
|
+
};
|
|
1155
|
+
/** What a library transition produced, or the build refused with its reason. */
|
|
1156
|
+
const transitioned = (result) => result.ok ? result.value : failCustodyBuild(transitionFailureOf(result.error));
|
|
1157
|
+
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-execution-estimate.ts
|
|
1160
|
+
/**
|
|
1161
|
+
* Net memory units of one run of each handler: the largest figure the
|
|
1162
|
+
* budget table of contract b48545f's security review ("Resource
|
|
1163
|
+
* exhaustion": `aiken check -D` less each fixture's baseline, over the
|
|
1164
|
+
* largest state) gives for a transaction Lace builds, which issues one
|
|
1165
|
+
* grant, sweeps at most eight and spends at most twelve funds.
|
|
1166
|
+
*/
|
|
1167
|
+
const HANDLER_NET_MEMORY = {
|
|
1168
|
+
device: 48e4,
|
|
1169
|
+
spendWithGrant: 4e5,
|
|
1170
|
+
sweepGrant: 43e4,
|
|
1171
|
+
fund: 2e5,
|
|
1172
|
+
issueGrants: 15e4,
|
|
1173
|
+
burnGrants: 34e4,
|
|
1174
|
+
createAccount: 15e4,
|
|
1175
|
+
operate: 16e4
|
|
1176
|
+
};
|
|
1177
|
+
/**
|
|
1178
|
+
* Net memory units of the logic run per path, from the same table, and
|
|
1179
|
+
* what each issued grant, swept grant and fund input adds: the owner path
|
|
1180
|
+
* runs 1.11 M for a revoke, 1.99 M issuing one grant, 1.54 M sweeping one
|
|
1181
|
+
* and 1.21 M with a reserve beside the control; the agent path 1.96 M over
|
|
1182
|
+
* one deposit and 5.94 M over forty.
|
|
1183
|
+
*
|
|
1184
|
+
* Summed per transaction they reach at least the review's figures, given
|
|
1185
|
+
* in brackets: a creation 0.75 M (0.73 M), a revoke 1.59 M (1.33 M), an
|
|
1186
|
+
* issue 2.62 M (2.41 M), a sweep of eight 8.81 M (8.08 M) and an agent
|
|
1187
|
+
* spend over eight funds 4.76 M (4.17 M).
|
|
1188
|
+
*/
|
|
1189
|
+
const LOGIC_NET_MEMORY = {
|
|
1190
|
+
path: {
|
|
1191
|
+
agent: 196e4,
|
|
1192
|
+
arrival: 44e4,
|
|
1193
|
+
owner: 111e4
|
|
1194
|
+
},
|
|
1195
|
+
issuedGrant: 88e4,
|
|
1196
|
+
sweptGrant: 43e4,
|
|
1197
|
+
fund: 1e5
|
|
1198
|
+
};
|
|
1199
|
+
/**
|
|
1200
|
+
* What one run is expected to cost on chain, as a multiple of its net
|
|
1201
|
+
* figure. The runner leaves out decoding the script context, which every
|
|
1202
|
+
* run pays and which grows with the transaction. On upstream's devnet with
|
|
1203
|
+
* Preprod's parameters (docs/devnet-evidence.md, "Budget comparison") a
|
|
1204
|
+
* revoke over the largest state measured 1.74 M against 1.10 M net, a
|
|
1205
|
+
* sweep of eight 9.20 M against 8.26 M and an agent spend over twelve
|
|
1206
|
+
* deposits 4.28 M. A transaction that measures above the estimate is
|
|
1207
|
+
* reseeded once at the measurement.
|
|
1208
|
+
*/
|
|
1209
|
+
const CUSTODY_EXECUTION_ESTIMATE_FACTOR = 2;
|
|
1210
|
+
/**
|
|
1211
|
+
* CPU steps the estimate charges per memory unit. Every redeemer of the
|
|
1212
|
+
* devnet run measured between 290 and 460 steps per memory unit, the
|
|
1213
|
+
* smallest Fund runs at the top of that range; the large logic and proxy
|
|
1214
|
+
* runs, which dominate every total, stay below 370.
|
|
1215
|
+
*/
|
|
1216
|
+
const CUSTODY_ESTIMATE_STEPS_PER_MEMORY_UNIT = 400;
|
|
1217
|
+
/**
|
|
1218
|
+
* What a reseed adds to the measured execution units, in percent. The
|
|
1219
|
+
* measurement already holds the evaluation margin; this one covers the
|
|
1220
|
+
* reseeded transaction measuring differently, since a higher fee changes
|
|
1221
|
+
* its change output.
|
|
1222
|
+
*/
|
|
1223
|
+
const CUSTODY_RESEED_MARGIN_PERCENT = 10;
|
|
1224
|
+
const withReseedMargin = (units) => Math.ceil(units * (100 + CUSTODY_RESEED_MARGIN_PERCENT) / 100);
|
|
1225
|
+
const logicNetMemory = ({ logic, issueGrants = 0, sweepGrant = 0, fund = 0 }) => logic ? LOGIC_NET_MEMORY.path[logic] + LOGIC_NET_MEMORY.issuedGrant * issueGrants + LOGIC_NET_MEMORY.sweptGrant * sweepGrant + LOGIC_NET_MEMORY.fund * fund : 0;
|
|
1226
|
+
/**
|
|
1227
|
+
* The execution units a transaction that runs `runs` is expected to
|
|
1228
|
+
* measure: the handlers' and the logic's net memory units times the
|
|
1229
|
+
* estimate factor, and steps in proportion. It is not capped at the
|
|
1230
|
+
* per-transaction limit.
|
|
1231
|
+
*/
|
|
1232
|
+
const custodyExecutionEstimate = (runs) => {
|
|
1233
|
+
const memory = Object.keys(HANDLER_NET_MEMORY).reduce((total, handler) => total + HANDLER_NET_MEMORY[handler] * (runs[handler] ?? 0), logicNetMemory(runs)) * CUSTODY_EXECUTION_ESTIMATE_FACTOR;
|
|
1234
|
+
return {
|
|
1235
|
+
memory,
|
|
1236
|
+
steps: memory * CUSTODY_ESTIMATE_STEPS_PER_MEMORY_UNIT
|
|
1237
|
+
};
|
|
1238
|
+
};
|
|
1239
|
+
/**
|
|
1240
|
+
* The plan's estimate within the per-transaction limit, which no valid
|
|
1241
|
+
* transaction exceeds: what balancing prices a plan's fee at until a
|
|
1242
|
+
* reseed.
|
|
1243
|
+
*/
|
|
1244
|
+
const estimateWithinLimit = (estimate, limit) => ({
|
|
1245
|
+
memory: Math.min(estimate.memory, limit.memory),
|
|
1246
|
+
steps: Math.min(estimate.steps, limit.steps)
|
|
1247
|
+
});
|
|
1248
|
+
/** What a build learned from redeemers that measured above `seeded`. */
|
|
1249
|
+
const custodyReseedOf = (evaluated, seeded) => {
|
|
1250
|
+
const sum = evaluated.reduce((total, units) => ({
|
|
1251
|
+
memory: total.memory + units.memory,
|
|
1252
|
+
steps: total.steps + units.steps
|
|
1253
|
+
}), {
|
|
1254
|
+
memory: 0,
|
|
1255
|
+
steps: 0
|
|
1256
|
+
});
|
|
1257
|
+
return {
|
|
1258
|
+
measured: {
|
|
1259
|
+
memory: Math.max(sum.memory, seeded.memory),
|
|
1260
|
+
steps: Math.max(sum.steps, seeded.steps)
|
|
1261
|
+
},
|
|
1262
|
+
redeemers: evaluated.length,
|
|
1263
|
+
dearest: {
|
|
1264
|
+
memory: Math.max(0, ...evaluated.map(({ memory }) => memory)),
|
|
1265
|
+
steps: Math.max(0, ...evaluated.map(({ steps }) => steps))
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
};
|
|
1269
|
+
/**
|
|
1270
|
+
* What a reseed prices a plan of `redeemers` redeemers at: the measurement
|
|
1271
|
+
* plus the reseed margin, with each redeemer the plan has beyond the
|
|
1272
|
+
* measured transaction, such as a fund input the selection round added,
|
|
1273
|
+
* priced as the dearest one measured. It is not capped at the
|
|
1274
|
+
* per-transaction limit, so a transaction over the limit still builds and
|
|
1275
|
+
* its limit check reports the overrun.
|
|
1276
|
+
*/
|
|
1277
|
+
const reseededExecutionUnits = ({ measured, redeemers: measuredRedeemers, dearest }, redeemers) => {
|
|
1278
|
+
const added = Math.max(0, redeemers - measuredRedeemers);
|
|
1279
|
+
return {
|
|
1280
|
+
memory: withReseedMargin(measured.memory + added * dearest.memory),
|
|
1281
|
+
steps: withReseedMargin(measured.steps + added * dearest.steps)
|
|
1282
|
+
};
|
|
1283
|
+
};
|
|
1284
|
+
//#endregion
|
|
1285
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-assembly.ts
|
|
1286
|
+
/**
|
|
1287
|
+
* The account's control output: lovelace and the state NFT only, with the
|
|
1288
|
+
* state encoded afresh as an inline datum.
|
|
1289
|
+
*/
|
|
1290
|
+
const controlOutputOf = (identity, state, lovelace) => ({
|
|
1291
|
+
address: identity.address,
|
|
1292
|
+
value: {
|
|
1293
|
+
coins: lovelace,
|
|
1294
|
+
assets: new Map([[identity.stateNftAssetId, 1n]])
|
|
1295
|
+
},
|
|
1296
|
+
datum: require_outputs.encodeCustodyAccountState(state)
|
|
1297
|
+
});
|
|
1298
|
+
/**
|
|
1299
|
+
* A grant UTxO at the account: lovelace and the grant's token only, with
|
|
1300
|
+
* the grant as an inline datum and no reference script.
|
|
1301
|
+
*/
|
|
1302
|
+
const grantOutputOf = (identity, grant, lovelace) => ({
|
|
1303
|
+
address: identity.address,
|
|
1304
|
+
value: {
|
|
1305
|
+
coins: lovelace,
|
|
1306
|
+
assets: new Map([[require_tokens.custodyGrantAssetId({
|
|
1307
|
+
accountValidatorHash: identity.contractVersion.accountValidatorHash,
|
|
1308
|
+
stakeScriptHash: identity.stakeScriptHash
|
|
1309
|
+
}, grant.slot), 1n]])
|
|
1310
|
+
},
|
|
1311
|
+
datum: require_outputs.encodeCustodyGrant(grant)
|
|
1312
|
+
});
|
|
1313
|
+
/**
|
|
1314
|
+
* The least lovelace an output holds at its own size, at least `floor`. The
|
|
1315
|
+
* coin's own width changes the size, so the minimum is taken until it
|
|
1316
|
+
* settles.
|
|
1317
|
+
*/
|
|
1318
|
+
const outputLovelaceFor = (outputWith, floor, coinsPerUtxoByte) => {
|
|
1319
|
+
let lovelace = floor;
|
|
1320
|
+
for (;;) {
|
|
1321
|
+
const minimum = (0, _laceio_cardano_tx_construction.minAdaRequired)(outputWith(lovelace), BigInt(coinsPerUtxoByte));
|
|
1322
|
+
if (minimum <= lovelace) return lovelace;
|
|
1323
|
+
lovelace = minimum;
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
/**
|
|
1327
|
+
* The lovelace a control output carrying `state` holds: at least `floor`
|
|
1328
|
+
* and at least the minimum for its size.
|
|
1329
|
+
*/
|
|
1330
|
+
const controlLovelaceFor = ({ identity, state, floor }, coinsPerUtxoByte) => outputLovelaceFor((lovelace) => controlOutputOf(identity, state, lovelace), floor, coinsPerUtxoByte);
|
|
1331
|
+
/** What `units` of execution cost at the protocol's prices. */
|
|
1332
|
+
const executionFeeOf = (units, { prices }) => BigInt(Math.ceil(prices.memory * units.memory + prices.steps * units.steps));
|
|
1333
|
+
/**
|
|
1334
|
+
* The most the builder prices a fee at before evaluation, when it seeds
|
|
1335
|
+
* `seed` execution units: the largest transaction's size fee plus the seed
|
|
1336
|
+
* at the protocol's prices. Balancing must fund that fee from the inputs
|
|
1337
|
+
* before evaluation lowers it.
|
|
1338
|
+
*/
|
|
1339
|
+
const preEvaluationFeeBound = (parameters, seed) => BigInt(parameters.minFeeConstant) + BigInt(parameters.minFeeCoefficient) * BigInt(parameters.maxTxSize) + executionFeeOf(seed, parameters);
|
|
1340
|
+
/**
|
|
1341
|
+
* The least fee a plan's transaction pays, whatever its body and its
|
|
1342
|
+
* scripts measure: the size fee of the scripts it attaches and the
|
|
1343
|
+
* reference-script fee of the scripts its reference inputs carry.
|
|
1344
|
+
*/
|
|
1345
|
+
const leastFeeOf = (plan, parameters) => {
|
|
1346
|
+
const references = plan.referenceInputs ?? [];
|
|
1347
|
+
return BigInt(parameters.minFeeConstant) + BigInt(parameters.minFeeCoefficient) * BigInt(plan.scripts.reduce((bytes, script) => bytes + script.bytes.length / 2, 0)) + require_TransactionBuilder.referenceScriptFee(require_TransactionBuilder.referenceScriptSize(references.map(([txIn]) => txIn), references), parameters);
|
|
1348
|
+
};
|
|
1349
|
+
/**
|
|
1350
|
+
* The fee a plan is expected to balance at before evaluation, less the
|
|
1351
|
+
* bytes of its body beyond its scripts: its least fee and its seed
|
|
1352
|
+
* execution units at the protocol's prices. Fund selection aims at it; a
|
|
1353
|
+
* selection that falls short gains the next fund UTxO when balancing
|
|
1354
|
+
* fails, before any evaluation.
|
|
1355
|
+
*/
|
|
1356
|
+
const plannedFeeOf = (plan, parameters) => leastFeeOf(plan, parameters) + executionFeeOf(estimateWithinLimit(plan.executionEstimate, parameters.maxExecutionUnitsPerTransaction), parameters);
|
|
1357
|
+
const quantityOf = (utxo, assetId) => utxo[1].value.assets?.get(assetId) ?? 0n;
|
|
1358
|
+
const byCoinsDescending = (a, b) => a[1].value.coins === b[1].value.coins ? 0 : a[1].value.coins > b[1].value.coins ? -1 : 1;
|
|
1359
|
+
/**
|
|
1360
|
+
* Large-First selection over fund UTxOs: first the UTxOs holding most of
|
|
1361
|
+
* each requested asset, then the largest by lovelace, with `reserves` after
|
|
1362
|
+
* every fund. When the UTxOs cannot cover the target, everything is
|
|
1363
|
+
* selected, `covers` is false and the build decides: the target holds only
|
|
1364
|
+
* an estimate of the fee.
|
|
1365
|
+
*/
|
|
1366
|
+
const selectFunds = (funds, target, reserves = []) => {
|
|
1367
|
+
const remaining = [...[...funds].sort(byCoinsDescending), ...[...reserves].sort(byCoinsDescending)];
|
|
1368
|
+
const selected = [];
|
|
1369
|
+
const take = (utxo) => {
|
|
1370
|
+
remaining.splice(remaining.indexOf(utxo), 1);
|
|
1371
|
+
selected.push(utxo);
|
|
1372
|
+
};
|
|
1373
|
+
for (const [assetId, quantity] of target.assets ?? []) {
|
|
1374
|
+
let covered = selected.reduce((sum, utxo) => sum + quantityOf(utxo, assetId), 0n);
|
|
1375
|
+
while (covered < quantity) {
|
|
1376
|
+
const richest = remaining.reduce((best, utxo) => !best || quantityOf(utxo, assetId) > quantityOf(best, assetId) ? utxo : best, void 0);
|
|
1377
|
+
if (!richest || quantityOf(richest, assetId) === 0n) return {
|
|
1378
|
+
selected: [...selected, ...remaining],
|
|
1379
|
+
remaining: [],
|
|
1380
|
+
covers: false
|
|
1381
|
+
};
|
|
1382
|
+
take(richest);
|
|
1383
|
+
covered += quantityOf(richest, assetId);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
let lovelace = selected.reduce((sum, [, output]) => sum + output.value.coins, 0n);
|
|
1387
|
+
while (lovelace < target.coins && remaining.length > 0) {
|
|
1388
|
+
const [next] = remaining;
|
|
1389
|
+
take(next);
|
|
1390
|
+
lovelace += next[1].value.coins;
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
selected,
|
|
1394
|
+
remaining,
|
|
1395
|
+
covers: lovelace >= target.coins
|
|
1396
|
+
};
|
|
1397
|
+
};
|
|
1398
|
+
/** The sum of outputs' values. */
|
|
1399
|
+
const totalValueOf = (outputs) => {
|
|
1400
|
+
const assets = /* @__PURE__ */ new Map();
|
|
1401
|
+
let coins = 0n;
|
|
1402
|
+
for (const { value } of outputs) {
|
|
1403
|
+
coins += value.coins;
|
|
1404
|
+
for (const [assetId, quantity] of value.assets ?? []) assets.set(assetId, (assets.get(assetId) ?? 0n) + quantity);
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
coins,
|
|
1408
|
+
assets
|
|
1409
|
+
};
|
|
1410
|
+
};
|
|
1411
|
+
/**
|
|
1412
|
+
* The funds can no longer pay for the transaction; one more fund UTxO may.
|
|
1413
|
+
* Once none is left, the build fails with `exhausted`, or with
|
|
1414
|
+
* `insufficient-funds` when it is absent.
|
|
1415
|
+
*/
|
|
1416
|
+
var FundShortfall = class extends Error {
|
|
1417
|
+
constructor(message, exhausted) {
|
|
1418
|
+
super(message);
|
|
1419
|
+
this.exhausted = exhausted;
|
|
1420
|
+
this.name = "FundShortfall";
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
/**
|
|
1424
|
+
* The evaluated transaction needs a larger fee than its plan balanced at:
|
|
1425
|
+
* the scripts measured above the execution estimate. Building again seeded
|
|
1426
|
+
* from `reseed` funds it.
|
|
1427
|
+
*/
|
|
1428
|
+
var ExecutionEstimateShortfall = class extends Error {
|
|
1429
|
+
constructor(reseed) {
|
|
1430
|
+
super("The scripts measure above the execution estimate");
|
|
1431
|
+
this.reseed = reseed;
|
|
1432
|
+
this.name = "ExecutionEstimateShortfall";
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
/**
|
|
1436
|
+
* The transaction needs a fee above the most it may be balanced at. `fee`
|
|
1437
|
+
* is the fee it needs, or, when even the fee of its body without execution
|
|
1438
|
+
* is above that most, that fee. `evaluatedRedeemers` holds the budgets the
|
|
1439
|
+
* fee was priced at when an evaluation priced it.
|
|
1440
|
+
*/
|
|
1441
|
+
var MaxBalancedFeeExceeded = class extends Error {
|
|
1442
|
+
constructor(fee, evaluatedRedeemers = []) {
|
|
1443
|
+
super(`The transaction needs a fee of ${fee} lovelace, above the most it may be balanced at`);
|
|
1444
|
+
this.fee = fee;
|
|
1445
|
+
this.evaluatedRedeemers = evaluatedRedeemers;
|
|
1446
|
+
this.name = "MaxBalancedFeeExceeded";
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
/** The builder balanced a draft above the most fee allowed; nothing evaluated it. */
|
|
1450
|
+
var DraftFeeAboveMax = class extends Error {
|
|
1451
|
+
constructor(fee) {
|
|
1452
|
+
super(`The draft is balanced at ${fee} lovelace, above the most allowed`);
|
|
1453
|
+
this.fee = fee;
|
|
1454
|
+
this.name = "DraftFeeAboveMax";
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
/** Stops a draft balanced above `maxFee` before the provider evaluates it. */
|
|
1458
|
+
const withMaxDraftFee = (evaluator, maxFee) => ({ evaluate: async (tx, resolvedInputs) => {
|
|
1459
|
+
if (tx.body.fee > maxFee) throw new DraftFeeAboveMax(tx.body.fee);
|
|
1460
|
+
return evaluator.evaluate(tx, resolvedInputs);
|
|
1461
|
+
} });
|
|
1462
|
+
/**
|
|
1463
|
+
* `seed` scaled so that its execution costs at most `fee` at the protocol's
|
|
1464
|
+
* prices, which are linear in the units.
|
|
1465
|
+
*/
|
|
1466
|
+
const seedPricedAt = (seed, fee, parameters) => {
|
|
1467
|
+
const seedFee = executionFeeOf(seed, parameters);
|
|
1468
|
+
if (seedFee === 0n) return seed;
|
|
1469
|
+
const scale = Number(fee) / Number(seedFee);
|
|
1470
|
+
return {
|
|
1471
|
+
memory: Math.floor(seed.memory * scale),
|
|
1472
|
+
steps: Math.floor(seed.steps * scale)
|
|
1473
|
+
};
|
|
1474
|
+
};
|
|
1475
|
+
/**
|
|
1476
|
+
* Seed changes that keep a draft within the most fee it may be balanced
|
|
1477
|
+
* at. A draft lands within a few bytes of the fee its seed aims at, so one
|
|
1478
|
+
* change usually suffices.
|
|
1479
|
+
*/
|
|
1480
|
+
const MAX_DRAFT_FEE_ROUNDS = 4;
|
|
1481
|
+
/**
|
|
1482
|
+
* The plain change output a collateral-mode build returns to the account:
|
|
1483
|
+
* the largest output at `address` after the planned ones, without a datum.
|
|
1484
|
+
* -1 when there is none.
|
|
1485
|
+
*/
|
|
1486
|
+
const changeIndexOf = (outputs, address, plannedCount) => {
|
|
1487
|
+
let changeIndex = -1;
|
|
1488
|
+
for (const [index, output] of outputs.entries()) if (index >= plannedCount && output.address === address && output.datum === void 0 && output.datumHash === void 0 && (changeIndex < 0 || output.value.coins > outputs[changeIndex].value.coins)) changeIndex = index;
|
|
1489
|
+
return changeIndex;
|
|
1490
|
+
};
|
|
1491
|
+
const withBody = (transaction, fields) => {
|
|
1492
|
+
const core = transaction.toCore();
|
|
1493
|
+
return _laceio_cardano_core.Serialization.Transaction.fromCore({
|
|
1494
|
+
...core,
|
|
1495
|
+
body: {
|
|
1496
|
+
...core.body,
|
|
1497
|
+
...fields
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
};
|
|
1501
|
+
const hasGrown = (rewritten, transaction) => rewritten.body().toCbor().length > transaction.body().toCbor().length;
|
|
1502
|
+
const grewBody = () => failCustodyBuild({
|
|
1503
|
+
code: "build-failed",
|
|
1504
|
+
message: "Moving lovelace between outputs grew the body"
|
|
1505
|
+
});
|
|
1506
|
+
/**
|
|
1507
|
+
* The transaction with `outputs` in place of its own. The fee was priced
|
|
1508
|
+
* for the body's size, so the body must not grow, as it would when a coin
|
|
1509
|
+
* crosses a CBOR width boundary. A smaller body keeps a fee that also pays
|
|
1510
|
+
* for the bytes it dropped.
|
|
1511
|
+
*/
|
|
1512
|
+
const withOutputs = (transaction, outputs) => {
|
|
1513
|
+
const rewritten = withBody(transaction, { outputs });
|
|
1514
|
+
return hasGrown(rewritten, transaction) ? grewBody() : rewritten;
|
|
1515
|
+
};
|
|
1516
|
+
/** `output` holding `coins` lovelace. */
|
|
1517
|
+
const withCoins = (output, coins) => ({
|
|
1518
|
+
...output,
|
|
1519
|
+
value: {
|
|
1520
|
+
...output.value,
|
|
1521
|
+
coins
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
/**
|
|
1525
|
+
* The transaction with `outputs` in place of its own and its fee raised by
|
|
1526
|
+
* `excess`. The declared collateral rises with the fee, taken from the
|
|
1527
|
+
* collateral return, which must keep its minimum ADA.
|
|
1528
|
+
*/
|
|
1529
|
+
const withExcessInFee = (transaction, { outputs, excess }, { collateralPercentage, coinsPerUtxoByte }) => {
|
|
1530
|
+
const { fee, totalCollateral, collateralReturn } = transaction.body().toCore();
|
|
1531
|
+
const raised = fee + excess;
|
|
1532
|
+
const required = (raised * BigInt(collateralPercentage) + 99n) / 100n;
|
|
1533
|
+
let collateral = {};
|
|
1534
|
+
if (totalCollateral !== void 0 && totalCollateral < required) {
|
|
1535
|
+
const returned = collateralReturn && withCoins(collateralReturn, collateralReturn.value.coins - (required - totalCollateral));
|
|
1536
|
+
if (!returned || returned.value.coins < (0, _laceio_cardano_tx_construction.minAdaRequired)(returned, BigInt(coinsPerUtxoByte))) return failCustodyBuild({
|
|
1537
|
+
code: "collateral-insufficient",
|
|
1538
|
+
message: `The collateral cannot cover a fee of ${raised} lovelace`
|
|
1539
|
+
});
|
|
1540
|
+
collateral = {
|
|
1541
|
+
totalCollateral: required,
|
|
1542
|
+
collateralReturn: returned
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
const rewritten = withBody(transaction, {
|
|
1546
|
+
outputs,
|
|
1547
|
+
fee: raised,
|
|
1548
|
+
...collateral
|
|
1549
|
+
});
|
|
1550
|
+
return hasGrown(rewritten, transaction) ? grewBody() : rewritten;
|
|
1551
|
+
};
|
|
1552
|
+
/**
|
|
1553
|
+
* Moves the lovelace the builder's fee correction credited to a planned
|
|
1554
|
+
* output back to the change. The correction refunds the largest output at
|
|
1555
|
+
* the change address, and in collateral mode the planned account outputs
|
|
1556
|
+
* share that address: a control output would lock the lovelace for the
|
|
1557
|
+
* account's life, and a grant output must hold what was planned.
|
|
1558
|
+
*
|
|
1559
|
+
* The change takes it unless that grows the body the fee was priced for,
|
|
1560
|
+
* which happens only when its lovelace crosses 2^32; the fee then keeps
|
|
1561
|
+
* the saving instead.
|
|
1562
|
+
*
|
|
1563
|
+
* @throws `FundShortfall` when no change output exists to take it
|
|
1564
|
+
*/
|
|
1565
|
+
const restorePlannedOutputs = (transaction, plan, parameters) => {
|
|
1566
|
+
const outputs = [...transaction.body().toCore().outputs];
|
|
1567
|
+
let excess = 0n;
|
|
1568
|
+
for (const [index, planned] of plan.outputs.entries()) {
|
|
1569
|
+
const credited = outputs[index].value.coins - planned.value.coins;
|
|
1570
|
+
if (credited > 0n) {
|
|
1571
|
+
excess += credited;
|
|
1572
|
+
outputs[index] = withCoins(outputs[index], planned.value.coins);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (excess === 0n) return transaction;
|
|
1576
|
+
const changeIndex = changeIndexOf(outputs, plan.changeAddress, plan.outputs.length);
|
|
1577
|
+
if (changeIndex < 0) throw new FundShortfall("No change output to return the fee saving to");
|
|
1578
|
+
const credited = [...outputs];
|
|
1579
|
+
credited[changeIndex] = withCoins(outputs[changeIndex], outputs[changeIndex].value.coins + excess);
|
|
1580
|
+
const rewritten = withBody(transaction, { outputs: credited });
|
|
1581
|
+
return hasGrown(rewritten, transaction) ? withExcessInFee(transaction, {
|
|
1582
|
+
outputs,
|
|
1583
|
+
excess
|
|
1584
|
+
}, parameters) : rewritten;
|
|
1585
|
+
};
|
|
1586
|
+
/**
|
|
1587
|
+
* The lovelace the first output of a plan whose first output pays the fee
|
|
1588
|
+
* must hold to balance at the fee the builder settled on.
|
|
1589
|
+
*/
|
|
1590
|
+
var FirstOutputCoinsMissed = class extends Error {
|
|
1591
|
+
constructor(coins) {
|
|
1592
|
+
super(`The first output must hold ${coins} lovelace to balance`);
|
|
1593
|
+
this.coins = coins;
|
|
1594
|
+
this.name = "FirstOutputCoinsMissed";
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
/**
|
|
1598
|
+
* Coin selection for a plan whose first output pays the fee: it spends
|
|
1599
|
+
* exactly the preselected inputs and returns no change. The balancer prices
|
|
1600
|
+
* each target and asks again until the fee settles; when the settled target
|
|
1601
|
+
* still misses the inputs, the build stops with the first output's lovelace
|
|
1602
|
+
* that meets it.
|
|
1603
|
+
*/
|
|
1604
|
+
const firstOutputPaysSelector = (firstOutputCoins) => {
|
|
1605
|
+
let previousTarget;
|
|
1606
|
+
return { select: ({ preSelectedUtxo = [], targetValue }) => {
|
|
1607
|
+
const surplus = preSelectedUtxo.reduce((total, [, output]) => total + output.value.coins, 0n) - targetValue.coins;
|
|
1608
|
+
if (surplus !== 0n && previousTarget === targetValue.coins) throw new FirstOutputCoinsMissed(firstOutputCoins + surplus);
|
|
1609
|
+
previousTarget = targetValue.coins;
|
|
1610
|
+
return {
|
|
1611
|
+
selection: [...preSelectedUtxo],
|
|
1612
|
+
remaining: [],
|
|
1613
|
+
changeOutputs: []
|
|
1614
|
+
};
|
|
1615
|
+
} };
|
|
1616
|
+
};
|
|
1617
|
+
/**
|
|
1618
|
+
* Builds and balances one plan with the shared `TransactionBuilder`. Every
|
|
1619
|
+
* input is preselected, so balancing never adds an input without its
|
|
1620
|
+
* redeemer; the collateral is the sponsor's shared UTxO, returned to the
|
|
1621
|
+
* sponsor with total collateral set.
|
|
1622
|
+
*
|
|
1623
|
+
* @throws `FundShortfall` when the inputs cannot pay for the transaction
|
|
1624
|
+
* @throws `EvaluatedFeeShortfallError` when the scripts measure above `seed`
|
|
1625
|
+
* by more than the balanced fee covers
|
|
1626
|
+
*/
|
|
1627
|
+
const buildPlan = async (plan, chain, { evaluator, coinSelector, seed }) => {
|
|
1628
|
+
const parameters = chain.protocolParameters;
|
|
1629
|
+
const referenceInputs = plan.referenceInputs ?? [];
|
|
1630
|
+
const builder = new require_TransactionBuilder.TransactionBuilder(chain.chainId.networkMagic, parameters).setChangeAddress(plan.changeAddress).useCoinSelector(coinSelector).setCollateralUtxos([plan.collateral.utxo]).setCollateralChangeAddress(plan.collateral.returnAddress).setPlutusContext({
|
|
1631
|
+
costModels: parameters.costModels,
|
|
1632
|
+
txEvaluator: evaluator,
|
|
1633
|
+
inputResolver: require_util.createInputResolver([...plan.inputs.map(({ utxo }) => utxo), plan.collateral.utxo]),
|
|
1634
|
+
executionUnitsEstimate: seed
|
|
1635
|
+
}).setValidityInterval({
|
|
1636
|
+
...plan.invalidBefore !== void 0 && { invalidBefore: plan.invalidBefore },
|
|
1637
|
+
invalidHereafter: plan.invalidHereafter
|
|
1638
|
+
}).addRequiredSigner(plan.requiredSigner);
|
|
1639
|
+
for (const { utxo, redeemer } of plan.inputs) builder.addInput(utxo, redeemer ? { redeemer } : void 0);
|
|
1640
|
+
for (const utxo of referenceInputs) builder.addReferenceInput(utxo);
|
|
1641
|
+
for (const output of plan.outputs) builder.addOutput(output);
|
|
1642
|
+
for (const script of plan.scripts) builder.attachScript(script);
|
|
1643
|
+
if (plan.mint) builder.mintAssets(plan.mint.assets, plan.mint.redeemer);
|
|
1644
|
+
if (plan.registration) {
|
|
1645
|
+
const { stakeCredential, deposit, redeemer } = plan.registration;
|
|
1646
|
+
builder.addNewStakeAddressCertificate(stakeCredential, deposit, { redeemer });
|
|
1647
|
+
}
|
|
1648
|
+
if (plan.delegation) {
|
|
1649
|
+
const { poolId, stakeCredential, redeemer } = plan.delegation;
|
|
1650
|
+
builder.addStakeDelegationCertificate(poolId, stakeCredential, { redeemer });
|
|
1651
|
+
}
|
|
1652
|
+
for (const { rewardAccount, quantity, redeemer } of plan.withdrawals ?? []) builder.addScriptWithdrawal(rewardAccount, quantity, redeemer);
|
|
1653
|
+
for (const [label, metadatum] of plan.metadata ?? []) builder.setMetadata(label, metadatum);
|
|
1654
|
+
try {
|
|
1655
|
+
return await builder.build();
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
if (error instanceof require_TransactionBuilder.InputSelectionError) throw new FundShortfall(error.message);
|
|
1658
|
+
if (error instanceof require_TransactionBuilder.InsufficientCollateralError) return failCustodyBuild({
|
|
1659
|
+
code: "collateral-insufficient",
|
|
1660
|
+
message: error.message
|
|
1661
|
+
});
|
|
1662
|
+
throw error;
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
/**
|
|
1666
|
+
* Rounds of a plan whose first output pays the fee. The first round finds
|
|
1667
|
+
* the fee the builder settles on, the second lands on it; a third covers a
|
|
1668
|
+
* coin that crossed a CBOR width between them.
|
|
1669
|
+
*/
|
|
1670
|
+
const MAX_FIRST_OUTPUT_ROUNDS = 3;
|
|
1671
|
+
/**
|
|
1672
|
+
* Builds a plan whose first output pays the fee. The builder balances at a
|
|
1673
|
+
* fee priced for the seed execution units, so the first output is set to
|
|
1674
|
+
* what the inputs hold beyond that fee and the other outputs; once
|
|
1675
|
+
* evaluation lowers the fee, the builder refunds the saving to it, the
|
|
1676
|
+
* largest output at the change address.
|
|
1677
|
+
*
|
|
1678
|
+
* @throws `FundShortfall` when the inputs cannot pay the fee the builder
|
|
1679
|
+
* balances at
|
|
1680
|
+
*/
|
|
1681
|
+
const buildFeePaidByFirstOutput = async (plan, chain, { evaluator, seed }) => {
|
|
1682
|
+
const [first, ...rest] = plan.outputs;
|
|
1683
|
+
let coins = first.value.coins;
|
|
1684
|
+
for (let round = 1;; round++) try {
|
|
1685
|
+
return await buildPlan({
|
|
1686
|
+
...plan,
|
|
1687
|
+
outputs: [withCoins(first, coins), ...rest]
|
|
1688
|
+
}, chain, {
|
|
1689
|
+
evaluator,
|
|
1690
|
+
coinSelector: firstOutputPaysSelector(coins),
|
|
1691
|
+
seed
|
|
1692
|
+
});
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
if (!(error instanceof FirstOutputCoinsMissed) || round >= MAX_FIRST_OUTPUT_ROUNDS) throw error;
|
|
1695
|
+
if (error.coins < 0n) throw new FundShortfall("The first output cannot pay the fee the builder balances at");
|
|
1696
|
+
coins = error.coins;
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
/** How many redeemers a plan's transaction carries. */
|
|
1700
|
+
const redeemerCountOf = (plan) => plan.inputs.filter(({ redeemer }) => redeemer !== void 0).length + [
|
|
1701
|
+
plan.mint,
|
|
1702
|
+
plan.registration,
|
|
1703
|
+
plan.delegation
|
|
1704
|
+
].filter((part) => part !== void 0).length + (plan.withdrawals?.length ?? 0);
|
|
1705
|
+
/**
|
|
1706
|
+
* Builds a plan, adding the next fund UTxO each time the selected ones
|
|
1707
|
+
* cannot pay for it. Once none is left, it refuses with the shortfall's
|
|
1708
|
+
* reason, `insufficient-funds` by default. Planned outputs keep their
|
|
1709
|
+
* lovelace unless the first output pays the fee.
|
|
1710
|
+
*
|
|
1711
|
+
* With `maxBalancedFee`, a draft balanced above it gets a seed priced at
|
|
1712
|
+
* that much less, and an evaluated transaction that needs more than its
|
|
1713
|
+
* draft's fee gets a seed priced at what it needs, while that fits.
|
|
1714
|
+
*
|
|
1715
|
+
* @throws `ExecutionEstimateShortfall` when the scripts measure above the
|
|
1716
|
+
* seed by more than the balanced fee covers
|
|
1717
|
+
* @throws `MaxBalancedFeeExceeded` when the transaction needs a fee above
|
|
1718
|
+
* `maxBalancedFee`
|
|
1719
|
+
*/
|
|
1720
|
+
const assembleWithFunds = async ({ chain, funds, plan, evaluator, reseed, checkFunds, settle, maxBalancedFee }) => {
|
|
1721
|
+
const parameters = chain.protocolParameters;
|
|
1722
|
+
let { selected, remaining } = funds;
|
|
1723
|
+
let seedFeeLimit;
|
|
1724
|
+
let draftFeeRounds = 0;
|
|
1725
|
+
for (;;) {
|
|
1726
|
+
checkFunds?.(selected);
|
|
1727
|
+
const planned = plan(selected);
|
|
1728
|
+
const fullSeed = reseed ? reseededExecutionUnits(reseed, redeemerCountOf(planned)) : estimateWithinLimit(planned.executionEstimate, parameters.maxExecutionUnitsPerTransaction);
|
|
1729
|
+
const seed = seedFeeLimit === void 0 ? fullSeed : seedPricedAt(fullSeed, seedFeeLimit, parameters);
|
|
1730
|
+
const planEvaluator = maxBalancedFee === void 0 ? evaluator(selected) : withMaxDraftFee(evaluator(selected), maxBalancedFee);
|
|
1731
|
+
try {
|
|
1732
|
+
const built = planned.isFeePaidByFirstOutput ? await buildFeePaidByFirstOutput(planned, chain, {
|
|
1733
|
+
evaluator: planEvaluator,
|
|
1734
|
+
seed
|
|
1735
|
+
}) : restorePlannedOutputs(await buildPlan(planned, chain, {
|
|
1736
|
+
evaluator: planEvaluator,
|
|
1737
|
+
coinSelector: new require_TransactionBuilder.LargeFirstCoinSelector(),
|
|
1738
|
+
seed
|
|
1739
|
+
}), planned, parameters);
|
|
1740
|
+
return {
|
|
1741
|
+
transaction: settle ? settle(built) : built,
|
|
1742
|
+
funds: {
|
|
1743
|
+
selected,
|
|
1744
|
+
remaining
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
if (error instanceof DraftFeeAboveMax && maxBalancedFee !== void 0) {
|
|
1749
|
+
const seedFee = executionFeeOf(seed, parameters);
|
|
1750
|
+
const limit = seedFee - (error.fee - maxBalancedFee);
|
|
1751
|
+
if (limit < 0n) throw new MaxBalancedFeeExceeded(error.fee - seedFee);
|
|
1752
|
+
if (++draftFeeRounds > MAX_DRAFT_FEE_ROUNDS) throw new MaxBalancedFeeExceeded(error.fee);
|
|
1753
|
+
seedFeeLimit = limit;
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
if (error instanceof require_TransactionBuilder.EvaluatedFeeShortfallError) {
|
|
1757
|
+
if (maxBalancedFee !== void 0 && error.requiredFee > maxBalancedFee) throw new MaxBalancedFeeExceeded(error.requiredFee, error.evaluatedRedeemers);
|
|
1758
|
+
if (seedFeeLimit !== void 0 && ++draftFeeRounds <= MAX_DRAFT_FEE_ROUNDS) {
|
|
1759
|
+
seedFeeLimit = executionFeeOf(seed, parameters) + error.requiredFee - error.balancedFee;
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
throw new ExecutionEstimateShortfall(custodyReseedOf(error.evaluatedRedeemers.map(({ executionUnits }) => executionUnits), seed));
|
|
1763
|
+
}
|
|
1764
|
+
if (!(error instanceof FundShortfall)) throw error;
|
|
1765
|
+
if (remaining.length === 0) return failCustodyBuild(error.exhausted ?? {
|
|
1766
|
+
code: "insufficient-funds",
|
|
1767
|
+
message: error.message
|
|
1768
|
+
});
|
|
1769
|
+
selected = [...selected, remaining[0]];
|
|
1770
|
+
remaining = remaining.slice(1);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
const EVALUATION_MARGIN_PERCENT = 10n;
|
|
1775
|
+
/**
|
|
1776
|
+
* Pads a measured budget. The fee correction after evaluation changes the
|
|
1777
|
+
* body the scripts see, so the final transaction is not the one measured.
|
|
1778
|
+
*/
|
|
1779
|
+
const withEvaluationMargin = ({ memory, steps }) => ({
|
|
1780
|
+
memory: Number((BigInt(memory) * (100n + EVALUATION_MARGIN_PERCENT) + 99n) / 100n),
|
|
1781
|
+
steps: Number((BigInt(steps) * (100n + EVALUATION_MARGIN_PERCENT) + 99n) / 100n)
|
|
1782
|
+
});
|
|
1783
|
+
const atLeast = (budget, floor) => floor ? {
|
|
1784
|
+
memory: Math.max(budget.memory, floor.memory),
|
|
1785
|
+
steps: Math.max(budget.steps, floor.steps)
|
|
1786
|
+
} : budget;
|
|
1787
|
+
const redeemerKey = ({ purpose, index }) => `${purpose}:${index}`;
|
|
1788
|
+
/** A redeemer the provider did not evaluate fails, as it does for the sponsor service. */
|
|
1789
|
+
const unmeasured = ({ purpose, index }) => failCustodyBuild({
|
|
1790
|
+
code: "script-evaluation-failed",
|
|
1791
|
+
message: `The provider did not evaluate redeemer ${purpose}#${index}`
|
|
1792
|
+
});
|
|
1793
|
+
/**
|
|
1794
|
+
* Every redeemer whose declared budget is below what the provider measures,
|
|
1795
|
+
* with the measurement.
|
|
1796
|
+
*/
|
|
1797
|
+
const findUnderDeclaredBudgets = (transaction, measured) => {
|
|
1798
|
+
return (transaction.witnessSet().redeemers()?.toCore() ?? []).flatMap(({ purpose, index, executionUnits }) => {
|
|
1799
|
+
const measurement = measured.find((entry) => entry.purpose === purpose && entry.index === index) ?? unmeasured({
|
|
1800
|
+
purpose,
|
|
1801
|
+
index
|
|
1802
|
+
});
|
|
1803
|
+
return measurement.budget.memory > executionUnits.memory || measurement.budget.steps > executionUnits.steps ? [measurement] : [];
|
|
1804
|
+
});
|
|
1805
|
+
};
|
|
1806
|
+
/**
|
|
1807
|
+
* The outputs a transaction's inputs, reference inputs and collateral
|
|
1808
|
+
* resolve to, in that order, from the UTxOs the build chose them from.
|
|
1809
|
+
*/
|
|
1810
|
+
const resolvedInputsOf = (transaction, candidates) => {
|
|
1811
|
+
const body = transaction.body().toCore();
|
|
1812
|
+
const byOutpoint = new Map(candidates.map((utxo) => [require_util.outpointKey(utxo[0]), utxo]));
|
|
1813
|
+
return [
|
|
1814
|
+
...body.inputs,
|
|
1815
|
+
...body.referenceInputs ?? [],
|
|
1816
|
+
...body.collaterals ?? []
|
|
1817
|
+
].map((input) => byOutpoint.get(require_util.outpointKey(input))).filter((utxo) => utxo !== void 0);
|
|
1818
|
+
};
|
|
1819
|
+
/** A lovelace amount whose CBOR width every change output reaches. */
|
|
1820
|
+
const CHANGE_COIN_WIDTH_PROBE = 1000000n;
|
|
1821
|
+
/** The minimum ADA of a lovelace-only change output at an address. */
|
|
1822
|
+
const changeReserveAt = (address, coinsPerUtxoByte) => (0, _laceio_cardano_tx_construction.minAdaRequired)({
|
|
1823
|
+
address,
|
|
1824
|
+
value: { coins: CHANGE_COIN_WIDTH_PROBE }
|
|
1825
|
+
}, BigInt(coinsPerUtxoByte));
|
|
1826
|
+
const scriptCredentialOf = (hash) => ({
|
|
1827
|
+
type: _laceio_cardano_core.Cardano.CredentialType.ScriptHash,
|
|
1828
|
+
hash
|
|
1829
|
+
});
|
|
1830
|
+
//#endregion
|
|
1831
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-chain.ts
|
|
1832
|
+
/**
|
|
1833
|
+
* The slot clock of the era history. A time or slot before the first era
|
|
1834
|
+
* is read in the first era: custody transactions only name slots near the
|
|
1835
|
+
* present.
|
|
1836
|
+
*/
|
|
1837
|
+
const createSlotClock = (eraSummaries) => {
|
|
1838
|
+
const eraOf = (isStarted) => eraSummaries.filter(isStarted).at(-1) ?? eraSummaries[0];
|
|
1839
|
+
return {
|
|
1840
|
+
slotAt: (posixMs) => {
|
|
1841
|
+
const era = eraOf(({ start }) => start.time.getTime() <= posixMs);
|
|
1842
|
+
return _laceio_cardano_core.Cardano.Slot(era.start.slot + Math.floor((posixMs - era.start.time.getTime()) / era.parameters.slotLength));
|
|
1843
|
+
},
|
|
1844
|
+
slotStart: (slot) => {
|
|
1845
|
+
const era = eraOf(({ start }) => start.slot <= slot);
|
|
1846
|
+
return era.start.time.getTime() + (slot - era.start.slot) * era.parameters.slotLength;
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
};
|
|
1850
|
+
/**
|
|
1851
|
+
* One provider read, retried while transient. A failure stops the build as
|
|
1852
|
+
* `unavailable`, since nothing about the custody account is known yet.
|
|
1853
|
+
*/
|
|
1854
|
+
const readFromProvider = async (what, request) => {
|
|
1855
|
+
try {
|
|
1856
|
+
return await (0, rxjs.firstValueFrom)(require_unwrap_provider_result.unwrapProviderResult(request));
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
return failCustodyBuild({
|
|
1859
|
+
code: "unavailable",
|
|
1860
|
+
message: `${what}: ${messageOf(error)}`
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
const loadCustodyChain = async ({ chainId, eraSummaries, provider, now }) => {
|
|
1865
|
+
if (!chainId) return failCustodyBuild({
|
|
1866
|
+
code: "unavailable",
|
|
1867
|
+
message: "Cardano chain id not available"
|
|
1868
|
+
});
|
|
1869
|
+
if (!eraSummaries || eraSummaries.length === 0) return failCustodyBuild({
|
|
1870
|
+
code: "unavailable",
|
|
1871
|
+
message: "Cardano era summaries not available"
|
|
1872
|
+
});
|
|
1873
|
+
return {
|
|
1874
|
+
chainId,
|
|
1875
|
+
protocolParameters: await readFromProvider("Cardano protocol parameters", () => provider.getProtocolParameters({ chainId })),
|
|
1876
|
+
clock: createSlotClock(eraSummaries),
|
|
1877
|
+
now,
|
|
1878
|
+
provider
|
|
1879
|
+
};
|
|
1880
|
+
};
|
|
1881
|
+
/**
|
|
1882
|
+
* What the provider measures every redeemer of a transaction at. There is
|
|
1883
|
+
* no fallback budget: a script that fails is a build that fails, with the
|
|
1884
|
+
* provider's message. A transient failure, such as a rate limit or a
|
|
1885
|
+
* dropped connection, is retried like every other read and then reported
|
|
1886
|
+
* as `unavailable`, since it says nothing about the scripts.
|
|
1887
|
+
*/
|
|
1888
|
+
const evaluateCustodyTransaction = async (transaction, { provider, chainId }) => {
|
|
1889
|
+
const tx = transaction.toCbor();
|
|
1890
|
+
try {
|
|
1891
|
+
return await (0, rxjs.firstValueFrom)(require_unwrap_provider_result.unwrapProviderResult(() => provider.evaluateTx({ tx }, { chainId })));
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
return failCustodyBuild(require_is_retriable_error.isRetriableError(error) ? {
|
|
1894
|
+
code: "unavailable",
|
|
1895
|
+
message: `Transaction evaluation: ${messageOf(error)}`
|
|
1896
|
+
} : {
|
|
1897
|
+
code: "script-evaluation-failed",
|
|
1898
|
+
message: messageOf(error)
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
/**
|
|
1903
|
+
* The first slot that starts strictly after `posixMs`. A sweep of a grant
|
|
1904
|
+
* dead by expiry alone sets it as the lower validity bound, because the
|
|
1905
|
+
* validator needs the start of that bound to be after the expiry.
|
|
1906
|
+
*/
|
|
1907
|
+
const firstSlotStartingAfter = (posixMs, { clock }) => _laceio_cardano_core.Cardano.Slot(clock.slotAt(Number(posixMs)) + 1);
|
|
1908
|
+
/**
|
|
1909
|
+
* The upper validity bound a funding window allows: the slot holding
|
|
1910
|
+
* `validUntil`, which starts no later than it. The window must leave a slot
|
|
1911
|
+
* after the current one.
|
|
1912
|
+
*/
|
|
1913
|
+
const validitySlotFor = (validUntil, { clock, now }) => {
|
|
1914
|
+
const slot = Number.isFinite(validUntil) ? clock.slotAt(validUntil) : 0;
|
|
1915
|
+
return slot > clock.slotAt(now) ? _laceio_cardano_core.Cardano.Slot(slot) : failCustodyBuild({
|
|
1916
|
+
code: "validity-window",
|
|
1917
|
+
validUntil
|
|
1918
|
+
});
|
|
1919
|
+
};
|
|
1920
|
+
//#endregion
|
|
1921
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-creator-index.ts
|
|
1922
|
+
/**
|
|
1923
|
+
* Reads what the chain holds at one creator index of one contract version:
|
|
1924
|
+
* whether the stake credential of the creator key is registered and, when
|
|
1925
|
+
* it is, whether the account address holds the state NFT. A registered
|
|
1926
|
+
* credential without the NFT was registered by someone else first, which
|
|
1927
|
+
* blocks creation at that index forever.
|
|
1928
|
+
*/
|
|
1929
|
+
const readCustodyCreatorIndex = async ({ contractVersion, deviceAccountPublicKey, creatorIndex }, chain) => {
|
|
1930
|
+
const key = await require_grant_status.deriveCustodyDeviceKey(deviceAccountPublicKey, creatorIndex);
|
|
1931
|
+
if (!key.ok) return failCustodyBuild({
|
|
1932
|
+
code: "invalid-operation",
|
|
1933
|
+
reason: "invalid-device-key",
|
|
1934
|
+
message: `Cannot derive custody device key ${creatorIndex}: ${key.error.code}`
|
|
1935
|
+
});
|
|
1936
|
+
const identity = require_identity.deriveCustodyAccountIdentity({
|
|
1937
|
+
contractVersion,
|
|
1938
|
+
creatorKeyHash: key.value.keyHash,
|
|
1939
|
+
chainId: chain.chainId
|
|
1940
|
+
});
|
|
1941
|
+
if (!identity.ok) return failCustodyBuild({
|
|
1942
|
+
code: "unavailable",
|
|
1943
|
+
message: `Cannot derive a custody account on this chain: ${identity.error.code}`
|
|
1944
|
+
});
|
|
1945
|
+
const { rewardAccount, address, stateNftAssetId } = identity.value;
|
|
1946
|
+
const { isRegistered } = await readFromProvider("Custody reward account", () => chain.provider.getRewardAccountInfo({ rewardAccount: require_util.CardanoRewardAccount(rewardAccount) }, { chainId: chain.chainId }));
|
|
1947
|
+
if (!isRegistered) return {
|
|
1948
|
+
status: "unregistered",
|
|
1949
|
+
key: key.value,
|
|
1950
|
+
identity: identity.value
|
|
1951
|
+
};
|
|
1952
|
+
return (await readFromProvider("Custody account UTxOs", () => chain.provider.getUtxosAtAddress({ address: require_util.CardanoPaymentAddress(address) }, { chainId: chain.chainId }))).some(([, output]) => output.value.assets?.has(stateNftAssetId)) ? {
|
|
1953
|
+
status: "account",
|
|
1954
|
+
record: require_grant_status.createCustodyAccountRecord(identity.value)
|
|
1955
|
+
} : { status: "pre-registered" };
|
|
1956
|
+
};
|
|
1957
|
+
//#endregion
|
|
1958
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-evaluation.ts
|
|
1959
|
+
/** Rebuilds allowed after the final transaction measures above its budgets. */
|
|
1960
|
+
const MAX_EVALUATION_ROUNDS = 3;
|
|
1961
|
+
/**
|
|
1962
|
+
* Prices every redeemer at what the provider measures on the draft, with a
|
|
1963
|
+
* margin, and at least at a floor an earlier round measured on final bytes.
|
|
1964
|
+
*/
|
|
1965
|
+
const createProviderEvaluator = (chain, floors) => ({ evaluate: async (tx) => {
|
|
1966
|
+
const measured = await evaluateCustodyTransaction(_laceio_cardano_core.Serialization.Transaction.fromCore(tx), chain);
|
|
1967
|
+
return (tx.witness.redeemers ?? []).map(({ purpose, index }) => {
|
|
1968
|
+
const measurement = measured.find((entry) => entry.purpose === purpose && entry.index === index);
|
|
1969
|
+
if (!measurement) return unmeasured({
|
|
1970
|
+
purpose,
|
|
1971
|
+
index
|
|
1972
|
+
});
|
|
1973
|
+
return {
|
|
1974
|
+
purpose,
|
|
1975
|
+
index,
|
|
1976
|
+
budget: atLeast(withEvaluationMargin(measurement.budget), floors.get(redeemerKey({
|
|
1977
|
+
purpose,
|
|
1978
|
+
index
|
|
1979
|
+
})))
|
|
1980
|
+
};
|
|
1981
|
+
});
|
|
1982
|
+
} });
|
|
1983
|
+
/**
|
|
1984
|
+
* Builds a transaction whose budgets come from provider evaluation, then
|
|
1985
|
+
* evaluates the final bytes. Every redeemer must declare at least what the
|
|
1986
|
+
* final transaction measures, which is what the sponsor service checks
|
|
1987
|
+
* before it signs; a shortfall raises the budget and rebuilds.
|
|
1988
|
+
*
|
|
1989
|
+
* Balancing prices the fee at the plan's execution estimate, so the funds,
|
|
1990
|
+
* or the control output that pays, need to cover little more than the real
|
|
1991
|
+
* fee. When the scripts measure above the estimate by more than the
|
|
1992
|
+
* balanced fee covers, the plan is built once more, seeded at the
|
|
1993
|
+
* measurement plus a margin: a second selection round, which may add fund
|
|
1994
|
+
* UTxOs.
|
|
1995
|
+
*
|
|
1996
|
+
* @throws `MaxBalancedFeeExceeded` when `maxBalancedFee` is set and the
|
|
1997
|
+
* transaction needs a fee above it
|
|
1998
|
+
*/
|
|
1999
|
+
const buildWithEvaluatedBudgets = async (params) => {
|
|
2000
|
+
let floors = /* @__PURE__ */ new Map();
|
|
2001
|
+
let funds = params.funds;
|
|
2002
|
+
let reseed;
|
|
2003
|
+
for (let round = 1;;) {
|
|
2004
|
+
let built;
|
|
2005
|
+
try {
|
|
2006
|
+
built = await assembleWithFunds({
|
|
2007
|
+
...params,
|
|
2008
|
+
funds,
|
|
2009
|
+
reseed,
|
|
2010
|
+
evaluator: () => createProviderEvaluator(params.chain, floors)
|
|
2011
|
+
});
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
if (!(error instanceof ExecutionEstimateShortfall)) throw error;
|
|
2014
|
+
if (reseed) return failCustodyBuild({
|
|
2015
|
+
code: "script-evaluation-failed",
|
|
2016
|
+
message: "The scripts measure above the execution estimate after a reseed"
|
|
2017
|
+
});
|
|
2018
|
+
reseed = error.reseed;
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
funds = built.funds;
|
|
2022
|
+
const underDeclared = findUnderDeclaredBudgets(built.transaction, await evaluateCustodyTransaction(built.transaction, params.chain));
|
|
2023
|
+
if (underDeclared.length === 0) return built.transaction;
|
|
2024
|
+
if (round >= MAX_EVALUATION_ROUNDS) return failCustodyBuild({
|
|
2025
|
+
code: "script-evaluation-failed",
|
|
2026
|
+
message: `Declared budgets stay below the measured ones after ${round} rounds`
|
|
2027
|
+
});
|
|
2028
|
+
round++;
|
|
2029
|
+
floors = new Map([...floors, ...underDeclared.map((entry) => [redeemerKey(entry), withEvaluationMargin(entry.budget)])]);
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
//#endregion
|
|
2033
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-scripts.ts
|
|
2034
|
+
const scriptHashOf = (script) => _laceio_cardano_core.Serialization.Script.fromCore(script).hash();
|
|
2035
|
+
/**
|
|
2036
|
+
* The catalogue pins each parked UTxO's address and lovelace with its
|
|
2037
|
+
* outref, so the builder prices its script without a chain lookup; the
|
|
2038
|
+
* provider's evaluation of every build resolves the outref on chain, so a
|
|
2039
|
+
* UTxO the chain no longer holds fails the build before anyone signs.
|
|
2040
|
+
*/
|
|
2041
|
+
const custodyScriptWitnesses = (version, { chainId }, scripts) => {
|
|
2042
|
+
const parked = new Map(require_hex.custodyReferenceScriptUtxos(version, chainId.networkMagic).map((utxo) => [scriptHashOf(utxo[1].scriptReference), utxo]));
|
|
2043
|
+
const attached = [];
|
|
2044
|
+
const referenced = [];
|
|
2045
|
+
for (const script of scripts) {
|
|
2046
|
+
const reference = parked.get(scriptHashOf(script));
|
|
2047
|
+
if (reference) referenced.push(reference);
|
|
2048
|
+
else attached.push(script);
|
|
2049
|
+
}
|
|
2050
|
+
return {
|
|
2051
|
+
attached,
|
|
2052
|
+
referenced
|
|
2053
|
+
};
|
|
2054
|
+
};
|
|
2055
|
+
const LOGIC_RUN_REDEEMER = require_grant_status.encodeCustodyLogicRedeemer();
|
|
2056
|
+
/**
|
|
2057
|
+
* Reads the logic's reward account once per transaction, so every build
|
|
2058
|
+
* round of it draws the same amount. A credential that is not registered
|
|
2059
|
+
* fails the build as `logic-not-registered`: the ledger would refuse the
|
|
2060
|
+
* withdrawal only at submission, after the sponsor and the device signed.
|
|
2061
|
+
* Registration is read, not delegation: the logic delegates to no pool.
|
|
2062
|
+
*/
|
|
2063
|
+
const readCustodyLogicRun = async (logic, { chainId, provider }) => {
|
|
2064
|
+
const rewardAccount = _laceio_cardano_core.Cardano.RewardAccount.fromCredential({
|
|
2065
|
+
type: _laceio_cardano_core.Cardano.CredentialType.ScriptHash,
|
|
2066
|
+
hash: logic.hash
|
|
2067
|
+
}, chainId.networkId);
|
|
2068
|
+
const { isRegistered, withdrawableAmount } = await readFromProvider("Custody logic reward account", () => provider.getRewardAccountInfo({ rewardAccount: require_util.CardanoRewardAccount(rewardAccount) }, { chainId }));
|
|
2069
|
+
if (!isRegistered) return failCustodyBuild({
|
|
2070
|
+
code: "logic-not-registered",
|
|
2071
|
+
rewardAccount
|
|
2072
|
+
});
|
|
2073
|
+
return {
|
|
2074
|
+
logic,
|
|
2075
|
+
withdrawal: {
|
|
2076
|
+
rewardAccount,
|
|
2077
|
+
quantity: require_bignum_vo.BigNumber.valueOf(withdrawableAmount),
|
|
2078
|
+
redeemer: LOGIC_RUN_REDEEMER
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
};
|
|
2082
|
+
/**
|
|
2083
|
+
* The logic the account's control datum names. Lace operates no account
|
|
2084
|
+
* under a logic outside its contract version's catalogue, whose rules it
|
|
2085
|
+
* has not reviewed.
|
|
2086
|
+
*/
|
|
2087
|
+
const accountLogicOf = ({ identity, state }) => {
|
|
2088
|
+
const logic = require_hex.findCustodyLogic(identity.contractVersion, state.logic);
|
|
2089
|
+
return logic.ok ? logic.value : failCustodyBuild({
|
|
2090
|
+
code: "account-not-live",
|
|
2091
|
+
status: "invalid"
|
|
2092
|
+
});
|
|
2093
|
+
};
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-validation.ts
|
|
2096
|
+
const cborOf$1 = (data) => _laceio_cardano_core.Serialization.PlutusData.fromCore(data).toCbor();
|
|
2097
|
+
/**
|
|
2098
|
+
* What the sponsor service checks the transaction against, from the funding
|
|
2099
|
+
* it leased, the contract version of the account the transaction operates
|
|
2100
|
+
* and the chain.
|
|
2101
|
+
*/
|
|
2102
|
+
const sponsorRuleContextOf = (funding, contractVersion, { chainId, clock, now, protocolParameters }) => ({
|
|
2103
|
+
mode: funding.mode === "fee" ? {
|
|
2104
|
+
kind: "fee",
|
|
2105
|
+
feeUtxo: funding.feeUtxo,
|
|
2106
|
+
maxSponsoredLovelace: funding.maxSponsoredLovelace
|
|
2107
|
+
} : { kind: "collateral" },
|
|
2108
|
+
sponsorAddress: funding.sponsorAddress,
|
|
2109
|
+
collateralUtxo: funding.collateralUtxo,
|
|
2110
|
+
contractVersion,
|
|
2111
|
+
networkMagic: chainId.networkMagic,
|
|
2112
|
+
latestValiditySlot: clock.slotAt(funding.validUntil),
|
|
2113
|
+
currentSlot: clock.slotAt(now),
|
|
2114
|
+
collateralPercentage: protocolParameters.collateralPercentage,
|
|
2115
|
+
costModels: protocolParameters.costModels
|
|
2116
|
+
});
|
|
2117
|
+
/**
|
|
2118
|
+
* The largest fee the sponsor's shared collateral UTxO covers, or undefined
|
|
2119
|
+
* without a collateral percentage. The ledger needs collateral of the fee
|
|
2120
|
+
* times the percentage, and the sponsor needs the rest of the UTxO back in
|
|
2121
|
+
* a return output that holds its minimum ADA. For the service's 5 ADA UTxO,
|
|
2122
|
+
* a percentage of 150 and a base return address on Preprod, that is
|
|
2123
|
+
* 2,686,833 lovelace, below the 3,333,333 the UTxO covers without a return.
|
|
2124
|
+
*/
|
|
2125
|
+
const collateralFeeCeilingOf = ({ collateralUtxo: [, collateral], sponsorAddress }, { collateralPercentage, coinsPerUtxoByte }) => {
|
|
2126
|
+
if (collateralPercentage <= 0) return void 0;
|
|
2127
|
+
const returned = (0, _laceio_cardano_tx_construction.minAdaRequired)({
|
|
2128
|
+
address: sponsorAddress,
|
|
2129
|
+
value: { coins: collateral.value.coins }
|
|
2130
|
+
}, BigInt(coinsPerUtxoByte));
|
|
2131
|
+
const available = collateral.value.coins - returned;
|
|
2132
|
+
return available > 0n ? available * 100n / BigInt(collateralPercentage) : 0n;
|
|
2133
|
+
};
|
|
2134
|
+
const unexpected = (what) => failCustodyBuild({
|
|
2135
|
+
code: "build-failed",
|
|
2136
|
+
message: `The built transaction ${what}`
|
|
2137
|
+
});
|
|
2138
|
+
const isSameGrant = (a, b) => cborOf$1(require_outputs.encodeCustodyGrant(a)) === cborOf$1(require_outputs.encodeCustodyGrant(b));
|
|
2139
|
+
/** Checks the control output and the grant UTxOs the transaction writes. */
|
|
2140
|
+
const checkAccountOutputs = ({ controlOutput, issuedGrants, sweptGrants, grantSpend }, expectation) => {
|
|
2141
|
+
const { control } = expectation;
|
|
2142
|
+
if (!control) {
|
|
2143
|
+
if (controlOutput) unexpected("rewrites the control state");
|
|
2144
|
+
} else if (!controlOutput || cborOf$1(require_outputs.encodeCustodyAccountState(controlOutput.state)) !== cborOf$1(require_outputs.encodeCustodyAccountState(control.state))) unexpected("does not carry the intended account state");
|
|
2145
|
+
else if (controlOutput.lovelace !== control.lovelace) unexpected("does not lock the intended control lovelace");
|
|
2146
|
+
const planned = expectation.issuedGrants ?? [];
|
|
2147
|
+
if (!(issuedGrants.length === planned.length && issuedGrants.every(({ grant, lovelace }, index) => isSameGrant(grant, planned[index].grant) && lovelace === planned[index].lovelace))) unexpected("does not issue the intended grants");
|
|
2148
|
+
const plannedSwept = new Set(expectation.sweptSlots ?? []);
|
|
2149
|
+
if (!(sweptGrants.length === plannedSwept.size && sweptGrants.every(({ grant }) => plannedSwept.has(grant.slot)))) unexpected("does not sweep the intended grants");
|
|
2150
|
+
const plannedSpend = expectation.grantSpend;
|
|
2151
|
+
if (!(!plannedSpend || grantSpend?.grant.slot === plannedSpend.slot && isSameGrant(grantSpend.after, plannedSpend.after))) unexpected("does not spend the intended grant");
|
|
2152
|
+
};
|
|
2153
|
+
/** Checks the value leaving the account, its own tokens aside. */
|
|
2154
|
+
const checkOutflow = ({ netOutflow }, fee, expectation) => {
|
|
2155
|
+
if (!expectation.outflow) return;
|
|
2156
|
+
const { requested, withdrawal } = expectation.outflow;
|
|
2157
|
+
const assets = requested.assets ?? /* @__PURE__ */ new Map();
|
|
2158
|
+
const accountPolicy = expectation.record.accountValidatorHash;
|
|
2159
|
+
const leaving = [...netOutflow.assets].filter(([assetId]) => _laceio_cardano_core.Cardano.AssetId.getPolicyId(assetId) !== accountPolicy);
|
|
2160
|
+
if (!(netOutflow.lovelace === requested.coins + fee - withdrawal && leaving.length === assets.size && [...assets].every(([assetId, quantity]) => netOutflow.assets.get(assetId.toLowerCase()) === quantity))) unexpected("pays out more than requested");
|
|
2161
|
+
};
|
|
2162
|
+
/**
|
|
2163
|
+
* Checks the final, balanced transaction before anyone is asked to sign it:
|
|
2164
|
+
* the shared custody inspection (the hosted signer's consent rules), the
|
|
2165
|
+
* intended control output, issued grants, sweeps and grant spend, the
|
|
2166
|
+
* account's net
|
|
2167
|
+
* outflow, the signer, the fee the sponsor's collateral covers, the fee
|
|
2168
|
+
* sponsor service's policy and the protocol limits. Any failure refuses
|
|
2169
|
+
* the build.
|
|
2170
|
+
*
|
|
2171
|
+
* `resolvedInputs` resolves every input, reference input and collateral
|
|
2172
|
+
* input.
|
|
2173
|
+
*/
|
|
2174
|
+
const finalizeCustodyBuild = ({ transaction, resolvedInputs }, chain, expectation) => {
|
|
2175
|
+
const inspection = require_grant_status.inspectCustodyTransaction(transaction, resolvedInputs, {
|
|
2176
|
+
chainId: chain.chainId,
|
|
2177
|
+
now: chain.now,
|
|
2178
|
+
slotToPosixMs: chain.clock.slotStart,
|
|
2179
|
+
accountRecord: expectation.record,
|
|
2180
|
+
allowGrantSpend: expectation.grantSpend !== void 0,
|
|
2181
|
+
...expectation.deviceKeyHash && { deviceKeyHash: expectation.deviceKeyHash }
|
|
2182
|
+
});
|
|
2183
|
+
if (!inspection.ok) return failCustodyBuild({
|
|
2184
|
+
code: "inspection-refused",
|
|
2185
|
+
refusals: [...inspection.error.refusals]
|
|
2186
|
+
});
|
|
2187
|
+
const { accounts, requiredSigners, fee, validity } = inspection.value;
|
|
2188
|
+
const [account] = accounts;
|
|
2189
|
+
checkAccountOutputs(account, expectation);
|
|
2190
|
+
checkOutflow(account, fee, expectation);
|
|
2191
|
+
if (requiredSigners.length !== 1 || requiredSigners[0] !== expectation.signer) return unexpected("does not name exactly the intended signer");
|
|
2192
|
+
const { funding } = expectation;
|
|
2193
|
+
const maxFee = funding.mode === "collateral" ? collateralFeeCeilingOf(funding, chain.protocolParameters) : void 0;
|
|
2194
|
+
if (maxFee !== void 0 && fee > maxFee) return failCustodyBuild({
|
|
2195
|
+
code: "collateral-fee-ceiling",
|
|
2196
|
+
fee,
|
|
2197
|
+
maxFee
|
|
2198
|
+
});
|
|
2199
|
+
const sponsorViolations = findCustodySponsorRuleViolations(transaction, resolvedInputs, sponsorRuleContextOf(funding, account.contractVersion, chain));
|
|
2200
|
+
if (sponsorViolations.length > 0) return failCustodyBuild({
|
|
2201
|
+
code: "sponsor-rules",
|
|
2202
|
+
violations: sponsorViolations
|
|
2203
|
+
});
|
|
2204
|
+
try {
|
|
2205
|
+
assertWithinTransactionLimits(transaction, resolvedInputs, chain.protocolParameters);
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
if (!(error instanceof TransactionLimitsExceededError)) throw error;
|
|
2208
|
+
return failCustodyBuild({
|
|
2209
|
+
code: "transaction-limits",
|
|
2210
|
+
usage: error.usage,
|
|
2211
|
+
exceeded: error.exceeded
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
return {
|
|
2215
|
+
transaction: transaction.toCbor(),
|
|
2216
|
+
bodyHash: transaction.getId(),
|
|
2217
|
+
resolvedInputs,
|
|
2218
|
+
accountRecord: expectation.record,
|
|
2219
|
+
fee,
|
|
2220
|
+
invalidHereafter: validity.invalidHereafter.slot
|
|
2221
|
+
};
|
|
2222
|
+
};
|
|
2223
|
+
//#endregion
|
|
2224
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/create-custody-account.ts
|
|
2225
|
+
/**
|
|
2226
|
+
* Lovelace a creation leaves unused below the sponsor's per-transaction
|
|
2227
|
+
* limit. It absorbs a fee that comes out slightly higher on the round that
|
|
2228
|
+
* settles the control lovelace, so that round still fits the limit.
|
|
2229
|
+
*/
|
|
2230
|
+
const CUSTODY_CREATION_SPONSOR_MARGIN = 100000n;
|
|
2231
|
+
/**
|
|
2232
|
+
* Builds of one creation before the control lovelace settles. The first
|
|
2233
|
+
* build prices the largest fee its execution estimate allows, so it fits
|
|
2234
|
+
* any fee within that estimate; the second claims the headroom the real fee
|
|
2235
|
+
* leaves; the third covers a fee that then moved by more than the margin.
|
|
2236
|
+
*/
|
|
2237
|
+
const MAX_HEADROOM_ROUNDS = 3;
|
|
2238
|
+
/**
|
|
2239
|
+
* The first creator index from `startIndex` whose stake credential is not
|
|
2240
|
+
* registered. A registered credential whose address holds the state NFT is
|
|
2241
|
+
* an existing account the caller should reopen; one without it was
|
|
2242
|
+
* registered by someone else first, which blocks creation there forever,
|
|
2243
|
+
* so the scan moves past it.
|
|
2244
|
+
*/
|
|
2245
|
+
const findCreatorIndex = async ({ deviceAccountPublicKey, startIndex = 0 }, chain) => {
|
|
2246
|
+
for (let index = startIndex; index < startIndex + 100; index++) {
|
|
2247
|
+
const found = await readCustodyCreatorIndex({
|
|
2248
|
+
contractVersion: require_hex.CURRENT_CUSTODY_CONTRACT_VERSION,
|
|
2249
|
+
deviceAccountPublicKey,
|
|
2250
|
+
creatorIndex: index
|
|
2251
|
+
}, chain);
|
|
2252
|
+
if (found.status === "unregistered") return {
|
|
2253
|
+
index,
|
|
2254
|
+
key: found.key,
|
|
2255
|
+
identity: found.identity
|
|
2256
|
+
};
|
|
2257
|
+
if (found.status === "account") return failCustodyBuild({
|
|
2258
|
+
code: "account-exists",
|
|
2259
|
+
creatorIndex: index,
|
|
2260
|
+
record: found.record
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
return failCustodyBuild({
|
|
2264
|
+
code: "index-exhausted",
|
|
2265
|
+
startIndex,
|
|
2266
|
+
scanned: 100
|
|
2267
|
+
});
|
|
2268
|
+
};
|
|
2269
|
+
const smaller = (a, b) => a < b ? a : b;
|
|
2270
|
+
/**
|
|
2271
|
+
* The control lovelace a creation aims for once its fee is known: what the
|
|
2272
|
+
* sponsor's limit leaves after the fee, the stake deposit and the margin,
|
|
2273
|
+
* within what the fee UTxO can fund, but never less than the initial
|
|
2274
|
+
* state's minimum.
|
|
2275
|
+
*/
|
|
2276
|
+
const headroomControlLovelace = ({ maxSponsoredLovelace, deposit, fundable, minimum }, fee) => {
|
|
2277
|
+
const target = smaller(maxSponsoredLovelace - deposit - fee - CUSTODY_CREATION_SPONSOR_MARGIN, fundable);
|
|
2278
|
+
return target > minimum ? target : minimum;
|
|
2279
|
+
};
|
|
2280
|
+
/**
|
|
2281
|
+
* Builds the creation of a custody account in the sponsor's fee mode: the
|
|
2282
|
+
* leased fee UTxO pays the fee, the stake deposit and the control output,
|
|
2283
|
+
* and its change returns to the sponsor in one output. The state NFT is
|
|
2284
|
+
* minted with `CreateAccount`, the stake credential is registered in the
|
|
2285
|
+
* same transaction with a Conway registration certificate, which carries
|
|
2286
|
+
* the `Operate` witness the mint looks for, and the logic the state names
|
|
2287
|
+
* runs through its withdrawal, which validates the new state. The creator
|
|
2288
|
+
* key is the only device and the only required signer, and the state
|
|
2289
|
+
* starts under the current logic at generation 0 with no slot issued, none
|
|
2290
|
+
* revoked and none outstanding.
|
|
2291
|
+
*
|
|
2292
|
+
* The proxy and the logic are read from their parked UTxOs on a network
|
|
2293
|
+
* that has them, and attached elsewhere; the stake script, applied to the
|
|
2294
|
+
* creator key, is always attached. Whatever the logic run draws joins the
|
|
2295
|
+
* control output, so the sponsor pays the same.
|
|
2296
|
+
*/
|
|
2297
|
+
const buildCustodyCreation = async (operation, funding, chain) => {
|
|
2298
|
+
const startIndex = operation.startIndex ?? 0;
|
|
2299
|
+
if (!Number.isSafeInteger(startIndex) || startIndex < 0 || startIndex + 100 > 2147483648) return failCustodyBuild({
|
|
2300
|
+
code: "invalid-operation",
|
|
2301
|
+
reason: "invalid-start-index",
|
|
2302
|
+
message: `Invalid creator start index ${startIndex}`
|
|
2303
|
+
});
|
|
2304
|
+
const contractVersion = require_hex.CURRENT_CUSTODY_CONTRACT_VERSION;
|
|
2305
|
+
const currentLogic = require_hex.findCustodyLogic(contractVersion, contractVersion.currentLogicHash);
|
|
2306
|
+
if (!currentLogic.ok) throw new Error(`The contract version has no logic ${contractVersion.currentLogicHash}`);
|
|
2307
|
+
const logicRun = await readCustodyLogicRun(currentLogic.value, chain);
|
|
2308
|
+
const { index, key, identity } = await findCreatorIndex(operation, chain);
|
|
2309
|
+
const { protocolParameters: parameters } = chain;
|
|
2310
|
+
const state = transitioned(createInitialCustodyAccountState(key.keyHash, logicRun.logic.hash));
|
|
2311
|
+
const witnesses = custodyScriptWitnesses(contractVersion, chain, [contractVersion.accountValidator, logicRun.logic.script]);
|
|
2312
|
+
const drawn = logicRun.withdrawal.quantity;
|
|
2313
|
+
const deposit = BigInt(parameters.stakeKeyDeposit);
|
|
2314
|
+
const invalidHereafter = validitySlotFor(funding.validUntil, chain);
|
|
2315
|
+
const executionEstimate = custodyExecutionEstimate({
|
|
2316
|
+
createAccount: 1,
|
|
2317
|
+
operate: 1,
|
|
2318
|
+
logic: "arrival"
|
|
2319
|
+
});
|
|
2320
|
+
const largestFee = preEvaluationFeeBound(parameters, estimateWithinLimit(executionEstimate, parameters.maxExecutionUnitsPerTransaction));
|
|
2321
|
+
const bounds = {
|
|
2322
|
+
maxSponsoredLovelace: funding.maxSponsoredLovelace,
|
|
2323
|
+
deposit,
|
|
2324
|
+
fundable: funding.feeUtxo[1].value.coins - changeReserveAt(funding.sponsorAddress, parameters.coinsPerUtxoByte) - deposit - largestFee,
|
|
2325
|
+
minimum: controlLovelaceFor({
|
|
2326
|
+
identity,
|
|
2327
|
+
state,
|
|
2328
|
+
floor: 0n
|
|
2329
|
+
}, parameters.coinsPerUtxoByte)
|
|
2330
|
+
};
|
|
2331
|
+
const buildWith = async (controlLovelace) => buildWithEvaluatedBudgets({
|
|
2332
|
+
chain,
|
|
2333
|
+
funds: {
|
|
2334
|
+
selected: [],
|
|
2335
|
+
remaining: []
|
|
2336
|
+
},
|
|
2337
|
+
plan: () => ({
|
|
2338
|
+
inputs: [{ utxo: funding.feeUtxo }],
|
|
2339
|
+
referenceInputs: witnesses.referenced,
|
|
2340
|
+
outputs: [controlOutputOf(identity, state, controlLovelace + drawn)],
|
|
2341
|
+
changeAddress: funding.sponsorAddress,
|
|
2342
|
+
scripts: [...witnesses.attached, identity.stakeScript],
|
|
2343
|
+
executionEstimate,
|
|
2344
|
+
requiredSigner: key.keyHash,
|
|
2345
|
+
invalidHereafter,
|
|
2346
|
+
collateral: {
|
|
2347
|
+
utxo: funding.collateralUtxo,
|
|
2348
|
+
returnAddress: funding.sponsorAddress
|
|
2349
|
+
},
|
|
2350
|
+
mint: {
|
|
2351
|
+
assets: new Map([[identity.stateNftAssetId, 1n]]),
|
|
2352
|
+
redeemer: require_grant_status.encodeCustodyMintRedeemer({ kind: "createAccount" })
|
|
2353
|
+
},
|
|
2354
|
+
registration: {
|
|
2355
|
+
stakeCredential: scriptCredentialOf(identity.stakeScriptHash),
|
|
2356
|
+
deposit,
|
|
2357
|
+
redeemer: require_grant_status.encodeCustodyStakeRedeemer()
|
|
2358
|
+
},
|
|
2359
|
+
withdrawals: [logicRun.withdrawal]
|
|
2360
|
+
})
|
|
2361
|
+
});
|
|
2362
|
+
let controlLovelace = headroomControlLovelace(bounds, largestFee);
|
|
2363
|
+
let transaction;
|
|
2364
|
+
for (let round = 1;; round++) {
|
|
2365
|
+
transaction = await buildWith(controlLovelace);
|
|
2366
|
+
const fee = transaction.body().fee();
|
|
2367
|
+
const sponsoredLovelace = fee + deposit + controlLovelace;
|
|
2368
|
+
const isWithinLimit = sponsoredLovelace <= funding.maxSponsoredLovelace;
|
|
2369
|
+
const settled = headroomControlLovelace(bounds, fee);
|
|
2370
|
+
const isLastRound = round >= MAX_HEADROOM_ROUNDS || settled === controlLovelace;
|
|
2371
|
+
if (isWithinLimit && (settled < controlLovelace || isLastRound)) break;
|
|
2372
|
+
if (!isWithinLimit && isLastRound) return failCustodyBuild({
|
|
2373
|
+
code: "sponsor-limit",
|
|
2374
|
+
sponsoredLovelace,
|
|
2375
|
+
maxSponsoredLovelace: funding.maxSponsoredLovelace
|
|
2376
|
+
});
|
|
2377
|
+
controlLovelace = settled;
|
|
2378
|
+
}
|
|
2379
|
+
const record = require_grant_status.createCustodyAccountRecord(identity);
|
|
2380
|
+
return {
|
|
2381
|
+
...finalizeCustodyBuild({
|
|
2382
|
+
transaction,
|
|
2383
|
+
resolvedInputs: resolvedInputsOf(transaction, [
|
|
2384
|
+
funding.feeUtxo,
|
|
2385
|
+
...witnesses.referenced,
|
|
2386
|
+
funding.collateralUtxo
|
|
2387
|
+
])
|
|
2388
|
+
}, chain, {
|
|
2389
|
+
record,
|
|
2390
|
+
control: {
|
|
2391
|
+
state,
|
|
2392
|
+
lovelace: controlLovelace + drawn
|
|
2393
|
+
},
|
|
2394
|
+
signer: key.keyHash,
|
|
2395
|
+
deviceKeyHash: key.keyHash,
|
|
2396
|
+
funding
|
|
2397
|
+
}),
|
|
2398
|
+
creation: {
|
|
2399
|
+
creatorIndex: index,
|
|
2400
|
+
identity: {
|
|
2401
|
+
chainId: chain.chainId,
|
|
2402
|
+
accountValidatorHash: contractVersion.accountValidatorHash,
|
|
2403
|
+
creatorKeyHash: identity.creatorKeyHash,
|
|
2404
|
+
stakeScriptHash: identity.stakeScriptHash,
|
|
2405
|
+
address: identity.address,
|
|
2406
|
+
rewardAccount: identity.rewardAccount
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
};
|
|
2411
|
+
//#endregion
|
|
2412
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-account.ts
|
|
2413
|
+
/** Every UTxO the classification of a live account holds. */
|
|
2414
|
+
const utxosOf = (state) => [
|
|
2415
|
+
state.controlUtxo,
|
|
2416
|
+
...state.funds,
|
|
2417
|
+
...state.reserves.own,
|
|
2418
|
+
...state.reserves.foreign,
|
|
2419
|
+
...state.grants.map(({ utxo }) => utxo),
|
|
2420
|
+
...state.stranded,
|
|
2421
|
+
...state.anomalies.map(({ utxo }) => utxo)
|
|
2422
|
+
];
|
|
2423
|
+
/**
|
|
2424
|
+
* Resolves the account an operation runs on. The synced view must show it
|
|
2425
|
+
* live with a confirmed control UTxO, and the chain, read again now, must
|
|
2426
|
+
* hold that same control UTxO. Funds, reserves and grants come from that
|
|
2427
|
+
* fresh read, so a UTxO already spent on chain or still unconfirmed is never
|
|
2428
|
+
* selected; funds and reserves an unsettled transaction of this wallet
|
|
2429
|
+
* spends are left out too, so concurrent builds pick disjoint ones.
|
|
2430
|
+
*
|
|
2431
|
+
* Owner operations are never chained: while the synced control UTxO is the
|
|
2432
|
+
* output of a submitted transaction that has not settled, the build is
|
|
2433
|
+
* refused.
|
|
2434
|
+
*/
|
|
2435
|
+
const resolveCustodyAccount = async (accountId, snapshot, chain) => {
|
|
2436
|
+
const { account, accountState, settledUtxos } = snapshot;
|
|
2437
|
+
if (!account || account.blockchainSpecific.chainId.networkMagic !== chain.chainId.networkMagic) return failCustodyBuild({ code: "unknown-account" });
|
|
2438
|
+
const identity = require_identity.verifyCardanoCustodyAccountIdentity(account.blockchainSpecific);
|
|
2439
|
+
if (!identity.ok) return failCustodyBuild({ code: "unknown-account" });
|
|
2440
|
+
if (accountState?.status !== "live") return failCustodyBuild({
|
|
2441
|
+
code: "account-not-live",
|
|
2442
|
+
status: accountState?.status ?? "unsynced"
|
|
2443
|
+
});
|
|
2444
|
+
const controlKey = require_util.utxoKey(accountState.controlUtxo);
|
|
2445
|
+
if (!settledUtxos.some((utxo) => require_util.utxoKey(utxo) === controlKey)) return failCustodyBuild({ code: "operation-in-progress" });
|
|
2446
|
+
const onChain = require_grant_status.classifyCardanoCustodyAccount({
|
|
2447
|
+
account: account.blockchainSpecific,
|
|
2448
|
+
utxos: await readFromProvider("Custody account UTxOs", () => chain.provider.getUtxosAtAddress({
|
|
2449
|
+
address: require_util.CardanoPaymentAddress(identity.value.address),
|
|
2450
|
+
withReferenceScripts: true
|
|
2451
|
+
}, { chainId: chain.chainId })),
|
|
2452
|
+
withdrawableRewards: 0n,
|
|
2453
|
+
protocolParameters: chain.protocolParameters
|
|
2454
|
+
});
|
|
2455
|
+
if (onChain.status !== "live" || require_util.utxoKey(onChain.controlUtxo) !== controlKey) return failCustodyBuild({ code: "stale-control" });
|
|
2456
|
+
const inFlight = new Set(utxosOf(accountState).map(require_util.utxoKey));
|
|
2457
|
+
const spentInFlight = new Set(settledUtxos.map(require_util.utxoKey).filter((key) => !inFlight.has(key)));
|
|
2458
|
+
const isUnspent = (utxo) => !spentInFlight.has(require_util.utxoKey(utxo));
|
|
2459
|
+
return {
|
|
2460
|
+
accountId,
|
|
2461
|
+
identity: identity.value,
|
|
2462
|
+
record: require_grant_status.createCustodyAccountRecord(identity.value),
|
|
2463
|
+
device: account.blockchainSpecific.device,
|
|
2464
|
+
control: onChain.controlUtxo,
|
|
2465
|
+
state: onChain.state,
|
|
2466
|
+
funds: onChain.funds.filter(isUnspent),
|
|
2467
|
+
reserves: onChain.reserves.own.filter(isUnspent),
|
|
2468
|
+
grants: onChain.grants,
|
|
2469
|
+
spentInFlight
|
|
2470
|
+
};
|
|
2471
|
+
};
|
|
2472
|
+
/**
|
|
2473
|
+
* The device key that signs an owner operation: the bound device, listed in
|
|
2474
|
+
* the current state. An observer holds no device and no owner authority.
|
|
2475
|
+
*/
|
|
2476
|
+
const requireListedDevice = ({ device, state }) => {
|
|
2477
|
+
if (!device) return failCustodyBuild({ code: "observer-account" });
|
|
2478
|
+
const keyHash = device.keyHash.toLowerCase();
|
|
2479
|
+
return state.devices.some((listed) => listed.toLowerCase() === keyHash) ? device.keyHash : failCustodyBuild({
|
|
2480
|
+
code: "device-not-listed",
|
|
2481
|
+
keyHash: device.keyHash
|
|
2482
|
+
});
|
|
2483
|
+
};
|
|
2484
|
+
//#endregion
|
|
2485
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-requests.ts
|
|
2486
|
+
const outputProblem = (output, identity, coinsPerUtxoByte) => {
|
|
2487
|
+
const address = _laceio_cardano_core.Cardano.Address.fromString(output.address);
|
|
2488
|
+
if (address?.getNetworkId() !== identity.chainId.networkId) return "is not an address on the account network";
|
|
2489
|
+
if (address.getType() === _laceio_cardano_core.Cardano.AddressType.Byron) return "is a Byron address, which a Plutus transaction cannot pay";
|
|
2490
|
+
if (address.toBech32() === identity.address) return "pays the account itself";
|
|
2491
|
+
const accountPolicy = identity.contractVersion.accountValidatorHash;
|
|
2492
|
+
if ([...output.value.assets?.keys() ?? []].some((assetId) => _laceio_cardano_core.Cardano.AssetId.getPolicyId(assetId) === accountPolicy)) return "carries a custody account token";
|
|
2493
|
+
return output.value.coins < (0, _laceio_cardano_tx_construction.minAdaRequired)(output, BigInt(coinsPerUtxoByte)) ? "holds less than its minimum ADA" : void 0;
|
|
2494
|
+
};
|
|
2495
|
+
/**
|
|
2496
|
+
* Checks the outputs an owner or a grantee asks the account to pay. Each
|
|
2497
|
+
* goes to a Shelley address on the account's network other than the
|
|
2498
|
+
* account itself, carries no token of a custody account and holds its
|
|
2499
|
+
* minimum ADA: the build never tops a requested amount up from account
|
|
2500
|
+
* funds.
|
|
2501
|
+
*/
|
|
2502
|
+
const checkRequestedOutputs = (outputs, identity, coinsPerUtxoByte) => {
|
|
2503
|
+
for (const [outputIndex, output] of outputs.entries()) {
|
|
2504
|
+
const problem = outputProblem(output, identity, coinsPerUtxoByte);
|
|
2505
|
+
if (problem) failCustodyBuild({
|
|
2506
|
+
code: "invalid-operation",
|
|
2507
|
+
reason: "invalid-output",
|
|
2508
|
+
message: `Output ${outputIndex} ${problem}`
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
};
|
|
2512
|
+
const invalidMetadata = (message) => failCustodyBuild({
|
|
2513
|
+
code: "invalid-operation",
|
|
2514
|
+
reason: "invalid-metadata",
|
|
2515
|
+
message
|
|
2516
|
+
});
|
|
2517
|
+
/** The largest metadata label the ledger encodes: a 64-bit unsigned integer. */
|
|
2518
|
+
const MAX_METADATA_LABEL = 2n ** 64n - 1n;
|
|
2519
|
+
/**
|
|
2520
|
+
* The metadata an owner asks the transaction to carry: a map of labels to
|
|
2521
|
+
* metadata the ledger accepts, which its serialization checks: text and
|
|
2522
|
+
* bytes hold at most 64 bytes each. An empty map carries nothing.
|
|
2523
|
+
*/
|
|
2524
|
+
const checkRequestedMetadata = (metadata) => {
|
|
2525
|
+
if (metadata === void 0) return void 0;
|
|
2526
|
+
if (!(metadata instanceof Map)) return invalidMetadata("The metadata must be a map of labels to metadata");
|
|
2527
|
+
if (metadata.size === 0) return void 0;
|
|
2528
|
+
for (const label of metadata.keys()) if (typeof label !== "bigint" || label < 0n || label > MAX_METADATA_LABEL) return invalidMetadata(`Metadata label ${String(label)} is not a 64-bit unsigned integer`);
|
|
2529
|
+
try {
|
|
2530
|
+
_laceio_cardano_core.Serialization.AuxiliaryData.fromCore({ blob: metadata });
|
|
2531
|
+
} catch (error) {
|
|
2532
|
+
return invalidMetadata(`The metadata cannot be encoded: ${error instanceof Error ? error.message : String(error)}`);
|
|
2533
|
+
}
|
|
2534
|
+
return metadata;
|
|
2535
|
+
};
|
|
2536
|
+
const invalidScope = (message) => failCustodyBuild({
|
|
2537
|
+
code: "invalid-operation",
|
|
2538
|
+
reason: "grant-scope",
|
|
2539
|
+
message
|
|
2540
|
+
});
|
|
2541
|
+
/**
|
|
2542
|
+
* The grant a scope request asks to issue in `slot`. The request goes
|
|
2543
|
+
* through the library's strict requested-scope parser, the one the signer
|
|
2544
|
+
* applies to `requestedScopes`, so Lace refuses a scope the signer would
|
|
2545
|
+
* refuse as malformed; its recipients, which must be on the account's
|
|
2546
|
+
* network, become Plutus addresses.
|
|
2547
|
+
*/
|
|
2548
|
+
const grantRequestOf = ({ scope: request, slot }, { chainId }, now) => {
|
|
2549
|
+
let serialized;
|
|
2550
|
+
try {
|
|
2551
|
+
serialized = require_grant_status.serializeCustodyRequestedGrantScope({
|
|
2552
|
+
...request,
|
|
2553
|
+
slot
|
|
2554
|
+
});
|
|
2555
|
+
} catch {
|
|
2556
|
+
return invalidScope("scope: not a grant scope");
|
|
2557
|
+
}
|
|
2558
|
+
const parsed = require_grant_status.parseCustodyRequestedGrantScope(serialized);
|
|
2559
|
+
if (!parsed.ok) return invalidScope([parsed.error.field, parsed.error.reason].filter(Boolean).join(": "));
|
|
2560
|
+
const { grantee, recipients, ...scope } = parsed.value;
|
|
2561
|
+
return {
|
|
2562
|
+
grantee,
|
|
2563
|
+
now,
|
|
2564
|
+
scope: {
|
|
2565
|
+
asset: scope.asset,
|
|
2566
|
+
perCallCap: scope.perCallCap,
|
|
2567
|
+
cap: scope.cap,
|
|
2568
|
+
lovelacePerCallCap: scope.lovelacePerCallCap,
|
|
2569
|
+
lovelaceCap: scope.lovelaceCap,
|
|
2570
|
+
expiresAt: scope.expiresAt,
|
|
2571
|
+
recipients: recipients.map((text) => {
|
|
2572
|
+
const address = _laceio_cardano_core.Cardano.Address.fromBech32(text);
|
|
2573
|
+
const recipient = require_outputs.custodyRecipientFromAddress(address);
|
|
2574
|
+
return recipient.ok && address.getNetworkId() === chainId.networkId ? recipient.value : invalidScope(`recipients: ${text} is not on the account network`);
|
|
2575
|
+
})
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
};
|
|
2579
|
+
/** A bech32 stake pool id, refused as an invalid operation when malformed. */
|
|
2580
|
+
const poolIdOf = (poolId) => {
|
|
2581
|
+
try {
|
|
2582
|
+
return _laceio_cardano_core.Cardano.PoolId(poolId);
|
|
2583
|
+
} catch {
|
|
2584
|
+
return failCustodyBuild({
|
|
2585
|
+
code: "invalid-operation",
|
|
2586
|
+
reason: "invalid-pool-id",
|
|
2587
|
+
message: `Invalid stake pool id ${poolId}`
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
//#endregion
|
|
2592
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-grant-spend.ts
|
|
2593
|
+
/**
|
|
2594
|
+
* The most fund UTxOs one grant spend spends, upstream's `MAX_FUND_INPUTS`.
|
|
2595
|
+
* Every input adds a proxy run that decodes the whole transaction, and the
|
|
2596
|
+
* logic checks each one: a spend over twelve deposits measured 4.28M
|
|
2597
|
+
* memory units on upstream's devnet, a quarter of Preprod's limit.
|
|
2598
|
+
*/
|
|
2599
|
+
const MAX_GRANT_SPEND_FUND_INPUTS = 12;
|
|
2600
|
+
/**
|
|
2601
|
+
* What the second build adds to the first build's fee when it lowers the
|
|
2602
|
+
* caps: room for a fee that moves when the caps' encoding or the measured
|
|
2603
|
+
* budgets change between the two builds.
|
|
2604
|
+
*/
|
|
2605
|
+
const GRANT_CAP_FEE_MARGIN = 10000n;
|
|
2606
|
+
const SPEND_WITH_GRANT_REDEEMER = require_grant_status.encodeCustodyAccountRedeemer({ kind: "spendWithGrant" });
|
|
2607
|
+
const FUND_REDEEMER$1 = require_grant_status.encodeCustodyAccountRedeemer({ kind: "fund" });
|
|
2608
|
+
const cborOf = (data) => _laceio_cardano_core.Serialization.PlutusData.fromCore(data).toCbor();
|
|
2609
|
+
const FUND_REDEEMER_CBOR = cborOf(FUND_REDEEMER$1);
|
|
2610
|
+
const SPEND_WITH_GRANT_REDEEMER_CBOR = cborOf(SPEND_WITH_GRANT_REDEEMER);
|
|
2611
|
+
const deltaOf = (value, fee = 0n) => ({
|
|
2612
|
+
lovelace: value.coins + fee,
|
|
2613
|
+
assets: new Map([...value.assets].map(([assetId, quantity]) => [assetId.toLowerCase(), quantity]))
|
|
2614
|
+
});
|
|
2615
|
+
/**
|
|
2616
|
+
* Metadata is an owner's to attach: the operation type leaves it out, and a
|
|
2617
|
+
* caller that names it anyway is refused rather than built without it.
|
|
2618
|
+
*/
|
|
2619
|
+
const refuseMetadata = (operation) => {
|
|
2620
|
+
if (operation.metadata !== void 0) failCustodyBuild({
|
|
2621
|
+
code: "invalid-operation",
|
|
2622
|
+
reason: "invalid-metadata",
|
|
2623
|
+
message: "A grant spend carries no metadata"
|
|
2624
|
+
});
|
|
2625
|
+
};
|
|
2626
|
+
/** The fee bound the operation names, or the default when it names none. */
|
|
2627
|
+
const feeBoundOf = ({ feeBound }) => {
|
|
2628
|
+
if (feeBound === void 0) return require_grant_status.CARDANO_CUSTODY_DEFAULT_GRANT_FEE_BOUND;
|
|
2629
|
+
return typeof feeBound === "bigint" && feeBound > 0n ? feeBound : failCustodyBuild({
|
|
2630
|
+
code: "invalid-operation",
|
|
2631
|
+
reason: "invalid-fee-bound",
|
|
2632
|
+
message: "The fee bound must be a positive number of lovelace"
|
|
2633
|
+
});
|
|
2634
|
+
};
|
|
2635
|
+
/**
|
|
2636
|
+
* The grant UTxO a grant spend spends: the grantee's grant in the named
|
|
2637
|
+
* slot, or its single current, unexpired grant. The grant must be current
|
|
2638
|
+
* against the control state, and no unsettled transaction of this wallet
|
|
2639
|
+
* may already spend its UTxO.
|
|
2640
|
+
*/
|
|
2641
|
+
const selectGrant = ({ grantee, slot }, { grants, state, spentInFlight }, { now }) => {
|
|
2642
|
+
const granteeHex = grantee.toLowerCase();
|
|
2643
|
+
const held = grants.filter(({ grant }) => grant.grantee.toLowerCase() === granteeHex);
|
|
2644
|
+
const livenessOf = ({ grant }) => require_grant_status.custodyGrantLiveness(grant, state, now);
|
|
2645
|
+
let selected;
|
|
2646
|
+
if (slot === void 0) {
|
|
2647
|
+
const current = held.filter((found) => livenessOf(found) === "current");
|
|
2648
|
+
if (current.length > 1) return failCustodyBuild({
|
|
2649
|
+
code: "grant-spend",
|
|
2650
|
+
reason: "ambiguous-grant",
|
|
2651
|
+
grantee,
|
|
2652
|
+
slots: current.map(({ grant }) => grant.slot)
|
|
2653
|
+
});
|
|
2654
|
+
selected = current[0] ?? held.at(-1);
|
|
2655
|
+
} else selected = held.find(({ grant }) => grant.slot === slot);
|
|
2656
|
+
if (!selected) return failCustodyBuild({
|
|
2657
|
+
code: "grant-spend",
|
|
2658
|
+
reason: "unknown-grant",
|
|
2659
|
+
grantee,
|
|
2660
|
+
...slot !== void 0 && { slot }
|
|
2661
|
+
});
|
|
2662
|
+
const liveness = livenessOf(selected);
|
|
2663
|
+
if (liveness !== "current") return failCustodyBuild({
|
|
2664
|
+
code: "grant-spend",
|
|
2665
|
+
reason: "dead-grant",
|
|
2666
|
+
slot: selected.grant.slot,
|
|
2667
|
+
liveness
|
|
2668
|
+
});
|
|
2669
|
+
return spentInFlight.has(require_util.outpointKey(selected.utxo[0])) ? failCustodyBuild({ code: "operation-in-progress" }) : selected;
|
|
2670
|
+
};
|
|
2671
|
+
/** Refuses an output the grant's recipient list does not name, on the account's network. */
|
|
2672
|
+
const checkRecipients = (grant, outputs, networkId) => {
|
|
2673
|
+
const { recipients } = grant.scope;
|
|
2674
|
+
if (recipients.length === 0) return;
|
|
2675
|
+
const allowed = new Set(recipients.flatMap((recipient) => {
|
|
2676
|
+
const address = require_outputs.custodyRecipientToAddress(recipient, networkId);
|
|
2677
|
+
return address.ok ? [address.value.toBech32()] : [];
|
|
2678
|
+
}));
|
|
2679
|
+
for (const [outputIndex, output] of outputs.entries()) {
|
|
2680
|
+
const address = _laceio_cardano_core.Cardano.Address.fromString(output.address).toBech32();
|
|
2681
|
+
if (!allowed.has(address)) failCustodyBuild({
|
|
2682
|
+
code: "grant-spend",
|
|
2683
|
+
reason: "recipient-not-allowed",
|
|
2684
|
+
outputIndex
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
/**
|
|
2689
|
+
* Refuses an outflow the grant's scope does not allow. `fee`, when known,
|
|
2690
|
+
* is the part of the lovelace outflow that pays the transaction fee; each
|
|
2691
|
+
* refusal reports it, so a caller can size a grant for the payment and the
|
|
2692
|
+
* fee.
|
|
2693
|
+
*/
|
|
2694
|
+
const refuseScope = (grant, leaving, fee) => {
|
|
2695
|
+
const violations = require_grant_status.findCustodyScopeViolations(grant.scope, leaving, fee);
|
|
2696
|
+
if (violations.length > 0) failCustodyBuild({
|
|
2697
|
+
code: "grant-spend",
|
|
2698
|
+
reason: "scope",
|
|
2699
|
+
violations
|
|
2700
|
+
});
|
|
2701
|
+
};
|
|
2702
|
+
/**
|
|
2703
|
+
* The upper validity bound of a grant spend: the earlier of the funding
|
|
2704
|
+
* window and the slot holding the grant's expiry, whose start the contract
|
|
2705
|
+
* compares with the expiry. A grant that expires within the current slot
|
|
2706
|
+
* leaves no such slot.
|
|
2707
|
+
*/
|
|
2708
|
+
const grantSpendValiditySlot = (grant, funding, chain) => {
|
|
2709
|
+
const windowSlot = validitySlotFor(funding.validUntil, chain);
|
|
2710
|
+
const { expiresAt } = grant.scope;
|
|
2711
|
+
if (expiresAt >= BigInt(chain.clock.slotStart(windowSlot))) return windowSlot;
|
|
2712
|
+
const expirySlot = chain.clock.slotAt(Number(expiresAt));
|
|
2713
|
+
return expirySlot > chain.clock.slotAt(chain.now) ? expirySlot : failCustodyBuild({
|
|
2714
|
+
code: "grant-spend",
|
|
2715
|
+
reason: "dead-grant",
|
|
2716
|
+
slot: grant.slot,
|
|
2717
|
+
liveness: "expired"
|
|
2718
|
+
});
|
|
2719
|
+
};
|
|
2720
|
+
/**
|
|
2721
|
+
* Refuses a spend the fund UTxOs cannot pay however they are combined:
|
|
2722
|
+
* they hold less of a requested asset, or less lovelace, with whatever the
|
|
2723
|
+
* logic run draws, than the outputs and the least fee. It runs before the
|
|
2724
|
+
* fund input cap, because consolidation cannot help an account that is
|
|
2725
|
+
* short of funds.
|
|
2726
|
+
*/
|
|
2727
|
+
const refuseShortFunds = (candidates, requested, { leastFee, drawn }) => {
|
|
2728
|
+
const available = totalValueOf(candidates.map(([, output]) => output));
|
|
2729
|
+
if (available.coins + drawn < requested.coins + leastFee || [...requested.assets].some(([assetId, quantity]) => (available.assets.get(assetId) ?? 0n) < quantity)) failCustodyBuild({
|
|
2730
|
+
code: "insufficient-funds",
|
|
2731
|
+
message: "The fund UTxOs hold less than the requested outputs and the least fee"
|
|
2732
|
+
});
|
|
2733
|
+
};
|
|
2734
|
+
const refuseFundInputCount = (funds) => {
|
|
2735
|
+
if (funds.length > MAX_GRANT_SPEND_FUND_INPUTS) failCustodyBuild({
|
|
2736
|
+
code: "grant-spend",
|
|
2737
|
+
reason: "fund-input-cap",
|
|
2738
|
+
maxFundInputs: MAX_GRANT_SPEND_FUND_INPUTS,
|
|
2739
|
+
requiredFundInputs: funds.length
|
|
2740
|
+
});
|
|
2741
|
+
};
|
|
2742
|
+
/**
|
|
2743
|
+
* Refuses a spend whose budgets exceed the per-transaction execution limit,
|
|
2744
|
+
* as a fund input count above what one spend fits: the grant's run is
|
|
2745
|
+
* fixed, so only fewer, larger fund UTxOs help. The count that fits follows
|
|
2746
|
+
* from the grant's and the logic's budgets and the dearest fund budget; the
|
|
2747
|
+
* logic's budget already counts every fund it checked, so the count errs
|
|
2748
|
+
* low.
|
|
2749
|
+
*/
|
|
2750
|
+
const refuseOverBudget = (redeemers, { maxExecutionUnitsPerTransaction: limit }) => {
|
|
2751
|
+
const total = redeemers.reduce((sum, { executionUnits }) => ({
|
|
2752
|
+
memory: sum.memory + executionUnits.memory,
|
|
2753
|
+
steps: sum.steps + executionUnits.steps
|
|
2754
|
+
}), {
|
|
2755
|
+
memory: 0,
|
|
2756
|
+
steps: 0
|
|
2757
|
+
});
|
|
2758
|
+
if (total.memory <= limit.memory && total.steps <= limit.steps) return;
|
|
2759
|
+
const fixed = redeemers.filter(({ purpose, data }) => purpose === _laceio_cardano_core.Cardano.RedeemerPurpose.withdrawal || cborOf(data) === SPEND_WITH_GRANT_REDEEMER_CBOR).reduce((sum, { executionUnits }) => ({
|
|
2760
|
+
memory: sum.memory + executionUnits.memory,
|
|
2761
|
+
steps: sum.steps + executionUnits.steps
|
|
2762
|
+
}), {
|
|
2763
|
+
memory: 0,
|
|
2764
|
+
steps: 0
|
|
2765
|
+
});
|
|
2766
|
+
const fundBudgets = redeemers.filter(({ data }) => cborOf(data) === FUND_REDEEMER_CBOR).map(({ executionUnits }) => executionUnits);
|
|
2767
|
+
const fundMemory = Math.max(1, ...fundBudgets.map(({ memory }) => memory));
|
|
2768
|
+
const fundSteps = Math.max(1, ...fundBudgets.map(({ steps }) => steps));
|
|
2769
|
+
const fitting = Math.floor(Math.min((limit.memory - fixed.memory) / fundMemory, (limit.steps - fixed.steps) / fundSteps));
|
|
2770
|
+
failCustodyBuild({
|
|
2771
|
+
code: "grant-spend",
|
|
2772
|
+
reason: "fund-input-cap",
|
|
2773
|
+
maxFundInputs: Math.max(0, Math.min(MAX_GRANT_SPEND_FUND_INPUTS, fitting)),
|
|
2774
|
+
requiredFundInputs: fundBudgets.length
|
|
2775
|
+
});
|
|
2776
|
+
};
|
|
2777
|
+
/**
|
|
2778
|
+
* The grant the recreated grant UTxO carries: the caps `grant_after_spend`
|
|
2779
|
+
* allows for the requested outputs and `fee`, and no lower than zero. The
|
|
2780
|
+
* logic accepts any cap from zero to the value for the fee of the
|
|
2781
|
+
* transaction it checks, so a `fee` no lower than that one writes valid
|
|
2782
|
+
* caps.
|
|
2783
|
+
*/
|
|
2784
|
+
const grantAfter = (grant, requested, fee) => {
|
|
2785
|
+
const { scope } = require_grant_status.custodyGrantAfterSpend(grant, deltaOf(requested, fee));
|
|
2786
|
+
return {
|
|
2787
|
+
...grant,
|
|
2788
|
+
scope: {
|
|
2789
|
+
...scope,
|
|
2790
|
+
cap: scope.cap > 0n ? scope.cap : 0n,
|
|
2791
|
+
lovelaceCap: scope.lovelaceCap > 0n ? scope.lovelaceCap : 0n
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
};
|
|
2795
|
+
/**
|
|
2796
|
+
* The most fee the outflow carries before it breaks a lovelace cap of the
|
|
2797
|
+
* scope: the lower of a lovelace grant's caps, or of a token grant's
|
|
2798
|
+
* lovelace caps, less the requested lovelace.
|
|
2799
|
+
*/
|
|
2800
|
+
const feeRoomOf = ({ scope }, requested) => {
|
|
2801
|
+
const [perCallCap, cap] = require_outputs.isCustodyLovelaceAsset(scope.asset) ? [scope.perCallCap, scope.cap] : [scope.lovelacePerCallCap, scope.lovelaceCap];
|
|
2802
|
+
return (perCallCap < cap ? perCallCap : cap) - requested.coins;
|
|
2803
|
+
};
|
|
2804
|
+
/**
|
|
2805
|
+
* Builds a grant spend for the grantee to sign. It references the
|
|
2806
|
+
* account's control UTxO, spends the grant UTxO with `SpendWithGrant` and
|
|
2807
|
+
* plain fund UTxOs with `Fund`, largest first, never a reserve, one
|
|
2808
|
+
* carrying a reference script, or one in `excludedInputs`, and runs the
|
|
2809
|
+
* logic the control names through its withdrawal. The grant UTxO is
|
|
2810
|
+
* recreated with the same value and the caps lowered by the outflow, the
|
|
2811
|
+
* requested outputs are paid, and the change, with whatever the logic run
|
|
2812
|
+
* draws, returns to the account as a plain output. The account pays the
|
|
2813
|
+
* fee, which counts against the grant's caps; the sponsor contributes only
|
|
2814
|
+
* its collateral. The proxy and the logic are read from their parked UTxOs
|
|
2815
|
+
* on a network that has them, and attached elsewhere.
|
|
2816
|
+
*
|
|
2817
|
+
* The caps go into the datum before the fee is known, and the logic counts
|
|
2818
|
+
* the fee in the outflow it checks against them, both on the final
|
|
2819
|
+
* transaction and on the balanced draft the provider evaluates. So every
|
|
2820
|
+
* draft is balanced at no more than the fee the caps were lowered by, nor
|
|
2821
|
+
* the fee that keeps the outflow within the caps, and two builds run with
|
|
2822
|
+
* evaluated budgets. The first lowers the caps by the fee bound, and a fee
|
|
2823
|
+
* above the bound or the caps is refused. The second lowers them by the
|
|
2824
|
+
* first fee and a small margin, and stands when its fee fits that; the
|
|
2825
|
+
* first build stands whenever the second cannot be built.
|
|
2826
|
+
*/
|
|
2827
|
+
const buildCustodyGrantSpend = async ({ operation, funding }, context, chain) => {
|
|
2828
|
+
const { identity, control } = context;
|
|
2829
|
+
const { protocolParameters: parameters } = chain;
|
|
2830
|
+
refuseMetadata(operation);
|
|
2831
|
+
const feeBound = feeBoundOf(operation);
|
|
2832
|
+
const spent = selectGrant(operation, context, chain);
|
|
2833
|
+
const { grant } = spent;
|
|
2834
|
+
checkRequestedOutputs(operation.outputs, identity, parameters.coinsPerUtxoByte);
|
|
2835
|
+
checkRecipients(grant, operation.outputs, identity.chainId.networkId);
|
|
2836
|
+
const requested = totalValueOf(operation.outputs);
|
|
2837
|
+
refuseScope(grant, deltaOf(requested));
|
|
2838
|
+
const invalidHereafter = grantSpendValiditySlot(grant, funding, chain);
|
|
2839
|
+
const grantee = grant.grantee;
|
|
2840
|
+
const logicRun = await readCustodyLogicRun(accountLogicOf(context), chain);
|
|
2841
|
+
const drawn = logicRun.withdrawal.quantity;
|
|
2842
|
+
const { contractVersion } = identity;
|
|
2843
|
+
const witnesses = custodyScriptWitnesses(contractVersion, chain, [contractVersion.accountValidator, logicRun.logic.script]);
|
|
2844
|
+
const excluded = new Set((operation.excludedInputs ?? []).map(require_util.outpointKey));
|
|
2845
|
+
const candidates = context.funds.filter(([input, output]) => output.scriptReference === void 0 && !excluded.has(require_util.outpointKey(input)));
|
|
2846
|
+
const planOf = (after) => (funds) => ({
|
|
2847
|
+
inputs: [{
|
|
2848
|
+
utxo: spent.utxo,
|
|
2849
|
+
redeemer: SPEND_WITH_GRANT_REDEEMER
|
|
2850
|
+
}, ...funds.map((utxo) => ({
|
|
2851
|
+
utxo,
|
|
2852
|
+
redeemer: FUND_REDEEMER$1
|
|
2853
|
+
}))],
|
|
2854
|
+
referenceInputs: [control, ...witnesses.referenced],
|
|
2855
|
+
outputs: [grantOutputOf(identity, after, spent.utxo[1].value.coins), ...operation.outputs],
|
|
2856
|
+
changeAddress: identity.address,
|
|
2857
|
+
scripts: witnesses.attached,
|
|
2858
|
+
executionEstimate: custodyExecutionEstimate({
|
|
2859
|
+
spendWithGrant: 1,
|
|
2860
|
+
fund: funds.length,
|
|
2861
|
+
logic: "agent"
|
|
2862
|
+
}),
|
|
2863
|
+
requiredSigner: grantee,
|
|
2864
|
+
invalidHereafter,
|
|
2865
|
+
collateral: {
|
|
2866
|
+
utxo: funding.collateralUtxo,
|
|
2867
|
+
returnAddress: funding.sponsorAddress
|
|
2868
|
+
},
|
|
2869
|
+
withdrawals: [logicRun.withdrawal]
|
|
2870
|
+
});
|
|
2871
|
+
const needed = requested.coins + plannedFeeOf(planOf(grant)([]), parameters) + changeReserveAt(identity.address, parameters.coinsPerUtxoByte) - drawn;
|
|
2872
|
+
const selection = selectFunds(candidates, {
|
|
2873
|
+
coins: needed > 0n ? needed : 0n,
|
|
2874
|
+
assets: requested.assets
|
|
2875
|
+
});
|
|
2876
|
+
if (!selection.covers) refuseShortFunds(candidates, requested, {
|
|
2877
|
+
leastFee: leastFeeOf(planOf(grant)([]), parameters),
|
|
2878
|
+
drawn
|
|
2879
|
+
});
|
|
2880
|
+
const feeRoom = feeRoomOf(grant, requested);
|
|
2881
|
+
/** A build whose caps are lowered by `lowering` and whose drafts fit them. */
|
|
2882
|
+
const build = async (lowering, funds) => {
|
|
2883
|
+
const after = grantAfter(grant, requested, lowering);
|
|
2884
|
+
const transaction = await buildWithEvaluatedBudgets({
|
|
2885
|
+
chain,
|
|
2886
|
+
funds,
|
|
2887
|
+
checkFunds: refuseFundInputCount,
|
|
2888
|
+
plan: planOf(after),
|
|
2889
|
+
maxBalancedFee: lowering < feeRoom ? lowering : feeRoom
|
|
2890
|
+
});
|
|
2891
|
+
return {
|
|
2892
|
+
transaction,
|
|
2893
|
+
after,
|
|
2894
|
+
fee: transaction.body().fee()
|
|
2895
|
+
};
|
|
2896
|
+
};
|
|
2897
|
+
/** Refuses a fee above the bound, or one that takes the outflow over a cap. */
|
|
2898
|
+
const refuseFee = (fee) => fee > feeBound ? failCustodyBuild({
|
|
2899
|
+
code: "grant-spend",
|
|
2900
|
+
reason: "fee-above-bound",
|
|
2901
|
+
fee,
|
|
2902
|
+
feeBound
|
|
2903
|
+
}) : failCustodyBuild({
|
|
2904
|
+
code: "grant-spend",
|
|
2905
|
+
reason: "scope",
|
|
2906
|
+
violations: require_grant_status.findCustodyScopeViolations(grant.scope, deltaOf(requested, fee), fee)
|
|
2907
|
+
});
|
|
2908
|
+
const first = await build(feeBound, selection).catch((error) => {
|
|
2909
|
+
if (!(error instanceof MaxBalancedFeeExceeded)) throw error;
|
|
2910
|
+
refuseOverBudget(error.evaluatedRedeemers, parameters);
|
|
2911
|
+
return refuseFee(error.fee);
|
|
2912
|
+
});
|
|
2913
|
+
refuseOverBudget(first.transaction.witnessSet().redeemers()?.toCore() ?? [], parameters);
|
|
2914
|
+
const firstFunds = resolvedInputsOf(first.transaction, candidates);
|
|
2915
|
+
let chosen = first;
|
|
2916
|
+
const tightFee = first.fee + GRANT_CAP_FEE_MARGIN;
|
|
2917
|
+
const { scope: tight } = grantAfter(grant, requested, tightFee);
|
|
2918
|
+
if (tight.cap > first.after.scope.cap || tight.lovelaceCap > first.after.scope.lovelaceCap) {
|
|
2919
|
+
const used = new Set(firstFunds.map(([input]) => require_util.outpointKey(input)));
|
|
2920
|
+
try {
|
|
2921
|
+
chosen = await build(tightFee, {
|
|
2922
|
+
selected: firstFunds,
|
|
2923
|
+
remaining: candidates.filter(([input]) => !used.has(require_util.outpointKey(input)))
|
|
2924
|
+
});
|
|
2925
|
+
} catch (error) {
|
|
2926
|
+
if (!(error instanceof MaxBalancedFeeExceeded) && !(error instanceof CustodyBuildFailure)) throw error;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
const { transaction, after } = chosen;
|
|
2930
|
+
return finalizeCustodyBuild({
|
|
2931
|
+
transaction,
|
|
2932
|
+
resolvedInputs: resolvedInputsOf(transaction, [
|
|
2933
|
+
spent.utxo,
|
|
2934
|
+
control,
|
|
2935
|
+
...witnesses.referenced,
|
|
2936
|
+
...candidates,
|
|
2937
|
+
funding.collateralUtxo
|
|
2938
|
+
])
|
|
2939
|
+
}, chain, {
|
|
2940
|
+
record: context.record,
|
|
2941
|
+
grantSpend: {
|
|
2942
|
+
slot: grant.slot,
|
|
2943
|
+
after
|
|
2944
|
+
},
|
|
2945
|
+
signer: grantee,
|
|
2946
|
+
outflow: {
|
|
2947
|
+
requested,
|
|
2948
|
+
withdrawal: drawn
|
|
2949
|
+
},
|
|
2950
|
+
funding
|
|
2951
|
+
});
|
|
2952
|
+
};
|
|
2953
|
+
//#endregion
|
|
2954
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/custody-owner-operations.ts
|
|
2955
|
+
/**
|
|
2956
|
+
* The control output headroom, its lovelace above the state's minimum, that
|
|
2957
|
+
* an issue or a replace tops up to. The headroom is the owner's fee reserve:
|
|
2958
|
+
* an owner operation the plain funds cannot pay takes its fee from it, so an
|
|
2959
|
+
* owner can still revoke a grant after its agent drains the plain funds.
|
|
2960
|
+
* It pays two revokes. Where the proxy and the logic are attached, as on
|
|
2961
|
+
* Mainnet, a revoke carries about 12.7 KB, 11,871 bytes of them the two
|
|
2962
|
+
* scripts, at 44 lovelace per byte. At Preprod's prices and the 1.6M to 1.8M
|
|
2963
|
+
* memory units upstream's devnet measured over the largest state, the
|
|
2964
|
+
* control pays a fee of about 0.84 ADA. Before evaluation balancing prices
|
|
2965
|
+
* a revoke at its execution estimate, about 0.99 ADA, which the draft the
|
|
2966
|
+
* provider evaluates must find in the headroom. The first revoke's fee, the
|
|
2967
|
+
* second's draft, the state's growth and the growth margin come to about
|
|
2968
|
+
* 2.04 ADA. On Preprod, where the scripts are referenced at 15 lovelace per
|
|
2969
|
+
* byte, a revoke costs about 0.5 ADA and its draft about 0.64 ADA.
|
|
2970
|
+
*/
|
|
2971
|
+
const CUSTODY_OWNER_FEE_RESERVE_TARGET = 2100000n;
|
|
2972
|
+
/**
|
|
2973
|
+
* The control datum bytes the control output keeps lovelace for beyond its
|
|
2974
|
+
* minimum after paying a fee. The largest growth one owner operation makes
|
|
2975
|
+
* is a device key: 30 bytes, its 28 bytes and their header. A revoked slot
|
|
2976
|
+
* adds at most 6, and a counter crossing a CBOR width at most 2. The next
|
|
2977
|
+
* operation's growth is then covered, whoever pays its fee.
|
|
2978
|
+
*/
|
|
2979
|
+
const CUSTODY_CONTROL_GROWTH_MARGIN_BYTES = 32;
|
|
2980
|
+
const DEVICE_REDEEMER = require_grant_status.encodeCustodyAccountRedeemer({ kind: "device" });
|
|
2981
|
+
const FUND_REDEEMER = require_grant_status.encodeCustodyAccountRedeemer({ kind: "fund" });
|
|
2982
|
+
const SWEEP_REDEEMER = require_grant_status.encodeCustodyAccountRedeemer({ kind: "sweepGrant" });
|
|
2983
|
+
const ISSUE_REDEEMER = require_grant_status.encodeCustodyMintRedeemer({ kind: "issueGrants" });
|
|
2984
|
+
const BURN_REDEEMER = require_grant_status.encodeCustodyMintRedeemer({ kind: "burnGrants" });
|
|
2985
|
+
/**
|
|
2986
|
+
* Refuses a grant whose per-call caps cannot pay the smallest spend: one
|
|
2987
|
+
* output at its minimum ADA and the default grant fee bound. Its agent could
|
|
2988
|
+
* never spend it.
|
|
2989
|
+
*/
|
|
2990
|
+
const spendableGrant = ({ grant }, { identity }, { protocolParameters }) => require_grant_status.isCardanoCustodyGrantExhausted(grant.scope, {
|
|
2991
|
+
protocolParameters,
|
|
2992
|
+
networkId: identity.chainId.networkId
|
|
2993
|
+
}) ? failCustodyBuild({
|
|
2994
|
+
code: "invalid-operation",
|
|
2995
|
+
reason: "grant-scope",
|
|
2996
|
+
message: "The caps cannot pay the smallest spend: one output at its minimum ADA and the grant fee bound"
|
|
2997
|
+
}) : grant;
|
|
2998
|
+
/** What an issue or a replace writes: the state and the grant it issues. */
|
|
2999
|
+
const issueEffectOf = (result, context, chain) => {
|
|
3000
|
+
const issued = transitioned(result);
|
|
3001
|
+
return {
|
|
3002
|
+
state: issued.state,
|
|
3003
|
+
outputs: [],
|
|
3004
|
+
issued: spendableGrant(issued, context, chain)
|
|
3005
|
+
};
|
|
3006
|
+
};
|
|
3007
|
+
/**
|
|
3008
|
+
* The sweep of the dead grants in `slots`. Each must be dead against the
|
|
3009
|
+
* state before the sweep. The validity interval starts no earlier than the
|
|
3010
|
+
* current slot, so a grant dead by expiry alone must expire before the
|
|
3011
|
+
* current slot starts; the interval then starts in the first slot after
|
|
3012
|
+
* the latest such expiry.
|
|
3013
|
+
*/
|
|
3014
|
+
const sweepEffectOf = (slots, { state, grants, spentInFlight }, chain) => {
|
|
3015
|
+
const swept = slots.map((slot) => {
|
|
3016
|
+
const found = grants.find(({ grant }) => grant.slot === slot);
|
|
3017
|
+
if (!found) return failCustodyBuild({
|
|
3018
|
+
code: "invalid-operation",
|
|
3019
|
+
reason: "state-transition",
|
|
3020
|
+
error: {
|
|
3021
|
+
code: "grant-not-found",
|
|
3022
|
+
slot
|
|
3023
|
+
}
|
|
3024
|
+
});
|
|
3025
|
+
return spentInFlight.has(require_util.outpointKey(found.utxo[0])) ? failCustodyBuild({ code: "operation-in-progress" }) : found;
|
|
3026
|
+
});
|
|
3027
|
+
const currentSlotStart = chain.clock.slotStart(chain.clock.slotAt(chain.now));
|
|
3028
|
+
const { state: next, startsAfter } = transitioned(sweepCustodyGrants(state, swept.map(({ grant }) => grant), currentSlotStart));
|
|
3029
|
+
return {
|
|
3030
|
+
state: next,
|
|
3031
|
+
outputs: [],
|
|
3032
|
+
swept: {
|
|
3033
|
+
grants: swept,
|
|
3034
|
+
...startsAfter !== void 0 && { invalidBefore: firstSlotStartingAfter(startsAfter, chain) }
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
};
|
|
3038
|
+
/**
|
|
3039
|
+
* The metadata an operation carries. Only an owner spend carries any: the
|
|
3040
|
+
* other operation types leave it out, and a caller that names it on one
|
|
3041
|
+
* anyway is refused rather than built without it, as a grant spend is.
|
|
3042
|
+
*/
|
|
3043
|
+
const metadataOf = (operation) => {
|
|
3044
|
+
const { metadata } = operation;
|
|
3045
|
+
if (metadata !== void 0 && operation.type !== "owner-spend") return failCustodyBuild({
|
|
3046
|
+
code: "invalid-operation",
|
|
3047
|
+
reason: "invalid-metadata",
|
|
3048
|
+
message: `A ${operation.type} carries no metadata`
|
|
3049
|
+
});
|
|
3050
|
+
return checkRequestedMetadata(metadata);
|
|
3051
|
+
};
|
|
3052
|
+
/**
|
|
3053
|
+
* The state an owner operation writes and what else it does. State changes
|
|
3054
|
+
* go through the library's transitions, which refuse what the account could
|
|
3055
|
+
* not use afterwards; withdrawal reads the reward balance now, because the
|
|
3056
|
+
* ledger accepts only the exact balance.
|
|
3057
|
+
*/
|
|
3058
|
+
const ownerEffectOf = async (operation, context, chain) => {
|
|
3059
|
+
const { identity, state } = context;
|
|
3060
|
+
const metadata = metadataOf(operation);
|
|
3061
|
+
const grants = context.grants.map(({ grant }) => grant);
|
|
3062
|
+
const requestOf = (scope) => grantRequestOf({
|
|
3063
|
+
scope,
|
|
3064
|
+
slot: state.nextSlot
|
|
3065
|
+
}, identity, chain.now);
|
|
3066
|
+
switch (operation.type) {
|
|
3067
|
+
case "issue-grant": return issueEffectOf(issueCustodyGrant(state, requestOf(operation.scope), grants), context, chain);
|
|
3068
|
+
case "replace-grant": return issueEffectOf(replaceCustodyGrant(state, {
|
|
3069
|
+
...requestOf(operation.scope),
|
|
3070
|
+
slot: operation.slot
|
|
3071
|
+
}, grants), context, chain);
|
|
3072
|
+
case "revoke-grant": return {
|
|
3073
|
+
state: transitioned(revokeCustodyGrant(state, operation.slot, grants)),
|
|
3074
|
+
outputs: []
|
|
3075
|
+
};
|
|
3076
|
+
case "revoke-all-grants": return {
|
|
3077
|
+
state: revokeAllCustodyGrants(state),
|
|
3078
|
+
outputs: []
|
|
3079
|
+
};
|
|
3080
|
+
case "sweep-grants": return sweepEffectOf(operation.slots, context, chain);
|
|
3081
|
+
case "add-device": return {
|
|
3082
|
+
state: transitioned(addCustodyDevice(state, operation.keyHash, grants)),
|
|
3083
|
+
outputs: []
|
|
3084
|
+
};
|
|
3085
|
+
case "remove-device": return {
|
|
3086
|
+
state: transitioned(removeCustodyDevice(state, operation.keyHash)),
|
|
3087
|
+
outputs: []
|
|
3088
|
+
};
|
|
3089
|
+
case "owner-spend": return {
|
|
3090
|
+
state,
|
|
3091
|
+
outputs: operation.outputs,
|
|
3092
|
+
...metadata && { metadata }
|
|
3093
|
+
};
|
|
3094
|
+
case "delegate": return {
|
|
3095
|
+
state,
|
|
3096
|
+
outputs: [],
|
|
3097
|
+
poolId: poolIdOf(operation.poolId)
|
|
3098
|
+
};
|
|
3099
|
+
case "withdraw-rewards": {
|
|
3100
|
+
const { withdrawableAmount } = await readFromProvider("Custody reward account", () => chain.provider.getRewardAccountInfo({ rewardAccount: require_util.CardanoRewardAccount(identity.rewardAccount) }, { chainId: chain.chainId }));
|
|
3101
|
+
const withdrawal = require_bignum_vo.BigNumber.valueOf(withdrawableAmount);
|
|
3102
|
+
return withdrawal > 0n ? {
|
|
3103
|
+
state,
|
|
3104
|
+
outputs: [],
|
|
3105
|
+
withdrawal
|
|
3106
|
+
} : failCustodyBuild({
|
|
3107
|
+
code: "invalid-operation",
|
|
3108
|
+
reason: "no-rewards",
|
|
3109
|
+
message: "The custody account has no rewards to withdraw"
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
const sweptGrantsOf = ({ effect }) => effect.swept?.grants ?? [];
|
|
3115
|
+
/**
|
|
3116
|
+
* The lovelace the withdrawals add to the transaction: the rewards an
|
|
3117
|
+
* operation withdraws and whatever the logic run draws. The change or the
|
|
3118
|
+
* control output keeps it at the account address.
|
|
3119
|
+
*/
|
|
3120
|
+
const withdrawnOf = ({ effect, logicRun }) => (effect.withdrawal ?? 0n) + logicRun.withdrawal.quantity;
|
|
3121
|
+
/** The transaction's plan with `funds` paying and the control output at `controlLovelace`. */
|
|
3122
|
+
const planOf = (build, funds, controlLovelace) => {
|
|
3123
|
+
const { context, chain, effect, signer, funding, issued, witnesses } = build;
|
|
3124
|
+
const { identity, control } = context;
|
|
3125
|
+
const swept = sweptGrantsOf(build);
|
|
3126
|
+
const isStakeOperation = effect.poolId !== void 0 || effect.withdrawal !== void 0;
|
|
3127
|
+
const mint = issued ? {
|
|
3128
|
+
assets: new Map([[require_tokens.custodyGrantAssetId(context.record, issued.grant.slot), 1n]]),
|
|
3129
|
+
redeemer: ISSUE_REDEEMER
|
|
3130
|
+
} : swept.length > 0 ? {
|
|
3131
|
+
assets: new Map(swept.map(({ token }) => [token, -1n])),
|
|
3132
|
+
redeemer: BURN_REDEEMER
|
|
3133
|
+
} : void 0;
|
|
3134
|
+
return {
|
|
3135
|
+
inputs: [
|
|
3136
|
+
{
|
|
3137
|
+
utxo: control,
|
|
3138
|
+
redeemer: DEVICE_REDEEMER
|
|
3139
|
+
},
|
|
3140
|
+
...swept.map(({ utxo }) => ({
|
|
3141
|
+
utxo,
|
|
3142
|
+
redeemer: SWEEP_REDEEMER
|
|
3143
|
+
})),
|
|
3144
|
+
...funds.map((utxo) => ({
|
|
3145
|
+
utxo,
|
|
3146
|
+
redeemer: FUND_REDEEMER
|
|
3147
|
+
}))
|
|
3148
|
+
],
|
|
3149
|
+
outputs: [
|
|
3150
|
+
controlOutputOf(identity, effect.state, controlLovelace),
|
|
3151
|
+
...issued ? [issued.output] : [],
|
|
3152
|
+
...effect.outputs
|
|
3153
|
+
],
|
|
3154
|
+
referenceInputs: witnesses.referenced,
|
|
3155
|
+
changeAddress: identity.address,
|
|
3156
|
+
scripts: [...witnesses.attached, ...isStakeOperation ? [identity.stakeScript] : []],
|
|
3157
|
+
executionEstimate: custodyExecutionEstimate({
|
|
3158
|
+
device: 1,
|
|
3159
|
+
sweepGrant: swept.length,
|
|
3160
|
+
fund: funds.length,
|
|
3161
|
+
issueGrants: issued ? 1 : 0,
|
|
3162
|
+
burnGrants: swept.length > 0 ? 1 : 0,
|
|
3163
|
+
operate: isStakeOperation ? 1 : 0,
|
|
3164
|
+
logic: "owner"
|
|
3165
|
+
}),
|
|
3166
|
+
requiredSigner: signer,
|
|
3167
|
+
...effect.swept?.invalidBefore !== void 0 && { invalidBefore: effect.swept.invalidBefore },
|
|
3168
|
+
invalidHereafter: validitySlotFor(funding.validUntil, chain),
|
|
3169
|
+
collateral: {
|
|
3170
|
+
utxo: funding.collateralUtxo,
|
|
3171
|
+
returnAddress: funding.sponsorAddress
|
|
3172
|
+
},
|
|
3173
|
+
...mint && { mint },
|
|
3174
|
+
...effect.poolId && { delegation: {
|
|
3175
|
+
stakeCredential: scriptCredentialOf(identity.stakeScriptHash),
|
|
3176
|
+
poolId: effect.poolId,
|
|
3177
|
+
redeemer: require_grant_status.encodeCustodyStakeRedeemer()
|
|
3178
|
+
} },
|
|
3179
|
+
withdrawals: [build.logicRun.withdrawal, ...effect.withdrawal ? [{
|
|
3180
|
+
rewardAccount: identity.rewardAccount,
|
|
3181
|
+
quantity: effect.withdrawal,
|
|
3182
|
+
redeemer: require_grant_status.encodeCustodyStakeRedeemer()
|
|
3183
|
+
}] : []],
|
|
3184
|
+
...effect.metadata && { metadata: effect.metadata }
|
|
3185
|
+
};
|
|
3186
|
+
};
|
|
3187
|
+
/**
|
|
3188
|
+
* Moves `amount` lovelace from the change into the control output once the
|
|
3189
|
+
* fee is final. A change left below its minimum ADA joins the control
|
|
3190
|
+
* output whole, when it holds no token.
|
|
3191
|
+
*
|
|
3192
|
+
* @throws `FundShortfall` when the change cannot spare `amount`; once no
|
|
3193
|
+
* fund is left the build fails with `owner-reserve-required`, or with
|
|
3194
|
+
* `insufficient-funds` when there is no change at all
|
|
3195
|
+
*/
|
|
3196
|
+
const topUpControl = (transaction, { context, chain, effect, issued }, amount) => {
|
|
3197
|
+
const outputs = [...transaction.body().toCore().outputs];
|
|
3198
|
+
const [control] = outputs;
|
|
3199
|
+
const changeIndex = changeIndexOf(outputs, context.identity.address, 1 + (issued ? 1 : 0) + effect.outputs.length);
|
|
3200
|
+
if (changeIndex < 0) throw new FundShortfall("No change output to top the reserve up from");
|
|
3201
|
+
const change = outputs[changeIndex];
|
|
3202
|
+
const left = change.value.coins - amount;
|
|
3203
|
+
const kept = (0, _laceio_cardano_tx_construction.minAdaRequired)(change, BigInt(chain.protocolParameters.coinsPerUtxoByte));
|
|
3204
|
+
const hasTokens = (change.value.assets?.size ?? 0) > 0;
|
|
3205
|
+
if (left >= kept) {
|
|
3206
|
+
outputs[0] = withCoins(control, control.value.coins + amount);
|
|
3207
|
+
outputs[changeIndex] = withCoins(change, left);
|
|
3208
|
+
} else if (!hasTokens && left >= 0n) {
|
|
3209
|
+
outputs[0] = withCoins(control, control.value.coins + change.value.coins);
|
|
3210
|
+
outputs.splice(changeIndex, 1);
|
|
3211
|
+
} else throw new FundShortfall("The change cannot top the owner fee reserve up", {
|
|
3212
|
+
code: "owner-reserve-required",
|
|
3213
|
+
target: CUSTODY_OWNER_FEE_RESERVE_TARGET,
|
|
3214
|
+
shortfall: amount - (hasTokens ? change.value.coins - kept : change.value.coins)
|
|
3215
|
+
});
|
|
3216
|
+
return {
|
|
3217
|
+
transaction: withOutputs(transaction, outputs),
|
|
3218
|
+
controlLovelace: outputs[0].value.coins
|
|
3219
|
+
};
|
|
3220
|
+
};
|
|
3221
|
+
/**
|
|
3222
|
+
* Plain funds, then reserves, largest first, pay the requested outputs, the
|
|
3223
|
+
* issued grant's lovelace, any growth of the control output and the fee, and
|
|
3224
|
+
* the change, rewards included, returns to the account without a datum. A
|
|
3225
|
+
* reserve's remainder becomes plain change. Swept grant lovelace goes to the
|
|
3226
|
+
* control output, and an issue tops the control headroom up to the owner fee
|
|
3227
|
+
* reserve target from the change.
|
|
3228
|
+
*
|
|
3229
|
+
* A fund carrying a reference script is never spent. Anyone can send one to
|
|
3230
|
+
* the account. Where the proxy and the logic are attached, such a fund
|
|
3231
|
+
* holding one of them would make the ledger refuse every owner operation
|
|
3232
|
+
* for providing the script twice, instead of falling back to the control
|
|
3233
|
+
* headroom; anywhere, its script would add to the fee and, for the fee
|
|
3234
|
+
* sponsor, to the languages of the script data hash.
|
|
3235
|
+
*/
|
|
3236
|
+
const buildPaidByFunds = async (build) => {
|
|
3237
|
+
const { context, chain, effect, issued, minimum } = build;
|
|
3238
|
+
const { identity } = context;
|
|
3239
|
+
const spendable = context.funds.filter(([, output]) => output.scriptReference === void 0);
|
|
3240
|
+
const { protocolParameters: parameters } = chain;
|
|
3241
|
+
const swept = sweptGrantsOf(build);
|
|
3242
|
+
const controlLovelace = build.spentAccountLovelace > minimum ? build.spentAccountLovelace : minimum;
|
|
3243
|
+
const reserveTarget = minimum + CUSTODY_OWNER_FEE_RESERVE_TARGET;
|
|
3244
|
+
const topUp = issued && reserveTarget > controlLovelace ? reserveTarget - controlLovelace : 0n;
|
|
3245
|
+
const requested = totalValueOf(effect.outputs);
|
|
3246
|
+
const needed = requested.coins + (issued?.output.value.coins ?? 0n) + (controlLovelace - build.spentAccountLovelace) + topUp + plannedFeeOf(planOf(build, [], controlLovelace), parameters) + changeReserveAt(identity.address, parameters.coinsPerUtxoByte) - withdrawnOf(build);
|
|
3247
|
+
let settledLovelace = controlLovelace;
|
|
3248
|
+
return {
|
|
3249
|
+
transaction: await buildWithEvaluatedBudgets({
|
|
3250
|
+
chain,
|
|
3251
|
+
funds: selectFunds(spendable, {
|
|
3252
|
+
coins: needed > 0n ? needed : 0n,
|
|
3253
|
+
assets: requested.assets
|
|
3254
|
+
}, context.reserves),
|
|
3255
|
+
plan: (funds) => planOf(build, funds, controlLovelace),
|
|
3256
|
+
...topUp > 0n && { settle: (built) => {
|
|
3257
|
+
const settled = topUpControl(built, build, topUp);
|
|
3258
|
+
settledLovelace = settled.controlLovelace;
|
|
3259
|
+
return settled.transaction;
|
|
3260
|
+
} }
|
|
3261
|
+
}),
|
|
3262
|
+
controlLovelace: settledLovelace,
|
|
3263
|
+
candidates: [
|
|
3264
|
+
...spendable,
|
|
3265
|
+
...context.reserves,
|
|
3266
|
+
...swept.map(({ utxo }) => utxo)
|
|
3267
|
+
]
|
|
3268
|
+
};
|
|
3269
|
+
};
|
|
3270
|
+
const headroomShortfall = () => failCustodyBuild({
|
|
3271
|
+
code: "insufficient-funds",
|
|
3272
|
+
message: "The plain funds and reserves cannot pay the fee, and the control output headroom cannot either"
|
|
3273
|
+
});
|
|
3274
|
+
/**
|
|
3275
|
+
* The control output pays the fee from its headroom: the transaction spends
|
|
3276
|
+
* only the control UTxO and any swept grant UTxOs, and the control output
|
|
3277
|
+
* takes everything they hold and whatever the withdrawals add, less the
|
|
3278
|
+
* fee. It must keep the new state's minimum and the growth margin, which a
|
|
3279
|
+
* headroom below the least fee never keeps, so such a build is refused
|
|
3280
|
+
* before any evaluation.
|
|
3281
|
+
*/
|
|
3282
|
+
const buildPaidByControl = async (build) => {
|
|
3283
|
+
const { chain, minimum, spentAccountLovelace } = build;
|
|
3284
|
+
const held = spentAccountLovelace + withdrawnOf(build);
|
|
3285
|
+
const plan = {
|
|
3286
|
+
...planOf(build, [], held),
|
|
3287
|
+
isFeePaidByFirstOutput: true
|
|
3288
|
+
};
|
|
3289
|
+
const floor = minimum + BigInt(CUSTODY_CONTROL_GROWTH_MARGIN_BYTES) * BigInt(chain.protocolParameters.coinsPerUtxoByte);
|
|
3290
|
+
if (held - leastFeeOf(plan, chain.protocolParameters) < floor) return headroomShortfall();
|
|
3291
|
+
const transaction = await buildWithEvaluatedBudgets({
|
|
3292
|
+
chain,
|
|
3293
|
+
funds: {
|
|
3294
|
+
selected: [],
|
|
3295
|
+
remaining: []
|
|
3296
|
+
},
|
|
3297
|
+
plan: () => plan
|
|
3298
|
+
});
|
|
3299
|
+
const controlLovelace = held - transaction.body().fee();
|
|
3300
|
+
if (controlLovelace < floor) return headroomShortfall();
|
|
3301
|
+
return {
|
|
3302
|
+
transaction,
|
|
3303
|
+
controlLovelace,
|
|
3304
|
+
candidates: sweptGrantsOf(build).map(({ utxo }) => utxo)
|
|
3305
|
+
};
|
|
3306
|
+
};
|
|
3307
|
+
const isInsufficientFunds = (error) => error instanceof CustodyBuildFailure && error.reason.code === "insufficient-funds";
|
|
3308
|
+
/**
|
|
3309
|
+
* Builds an owner operation in the sponsor's collateral mode: the account
|
|
3310
|
+
* pays and the sponsor contributes only its shared collateral. The control
|
|
3311
|
+
* UTxO is spent with `Device` and recreated from the resulting state, the
|
|
3312
|
+
* logic its state names runs through its withdrawal, and the bound device
|
|
3313
|
+
* key is the only required signer. The proxy and the logic are read from
|
|
3314
|
+
* their parked UTxOs on a network that has them, and attached elsewhere; a
|
|
3315
|
+
* stake operation attaches the stake script.
|
|
3316
|
+
*
|
|
3317
|
+
* Plain funds and reserves pay first. When they cannot, an operation that
|
|
3318
|
+
* pays nothing out and issues no grant takes its fee from the control
|
|
3319
|
+
* output's headroom instead.
|
|
3320
|
+
*/
|
|
3321
|
+
const buildCustodyOwnerOperation = async ({ operation, funding }, context, chain) => {
|
|
3322
|
+
const signer = requireListedDevice(context);
|
|
3323
|
+
const { identity, control } = context;
|
|
3324
|
+
const { coinsPerUtxoByte } = chain.protocolParameters;
|
|
3325
|
+
const effect = await ownerEffectOf(operation, context, chain);
|
|
3326
|
+
checkRequestedOutputs(effect.outputs, identity, coinsPerUtxoByte);
|
|
3327
|
+
const logicRun = await readCustodyLogicRun(accountLogicOf(context), chain);
|
|
3328
|
+
const { contractVersion } = identity;
|
|
3329
|
+
const swept = effect.swept?.grants ?? [];
|
|
3330
|
+
const { issued: grant } = effect;
|
|
3331
|
+
const issued = grant && {
|
|
3332
|
+
grant,
|
|
3333
|
+
output: grantOutputOf(identity, grant, outputLovelaceFor((lovelace) => grantOutputOf(identity, grant, lovelace), 0n, coinsPerUtxoByte))
|
|
3334
|
+
};
|
|
3335
|
+
const build = {
|
|
3336
|
+
context,
|
|
3337
|
+
chain,
|
|
3338
|
+
effect,
|
|
3339
|
+
signer,
|
|
3340
|
+
funding,
|
|
3341
|
+
...issued && { issued },
|
|
3342
|
+
minimum: controlLovelaceFor({
|
|
3343
|
+
identity,
|
|
3344
|
+
state: effect.state,
|
|
3345
|
+
floor: 0n
|
|
3346
|
+
}, coinsPerUtxoByte),
|
|
3347
|
+
spentAccountLovelace: swept.reduce((total, { utxo: [, output] }) => total + output.value.coins, control[1].value.coins),
|
|
3348
|
+
logicRun,
|
|
3349
|
+
witnesses: custodyScriptWitnesses(contractVersion, chain, [contractVersion.accountValidator, logicRun.logic.script])
|
|
3350
|
+
};
|
|
3351
|
+
const isControlPayable = effect.outputs.length === 0 && !issued;
|
|
3352
|
+
let paid;
|
|
3353
|
+
try {
|
|
3354
|
+
paid = await buildPaidByFunds(build);
|
|
3355
|
+
} catch (error) {
|
|
3356
|
+
if (!isControlPayable || !isInsufficientFunds(error)) throw error;
|
|
3357
|
+
paid = await buildPaidByControl(build);
|
|
3358
|
+
}
|
|
3359
|
+
const { transaction } = paid;
|
|
3360
|
+
return finalizeCustodyBuild({
|
|
3361
|
+
transaction,
|
|
3362
|
+
resolvedInputs: resolvedInputsOf(transaction, [
|
|
3363
|
+
control,
|
|
3364
|
+
...paid.candidates,
|
|
3365
|
+
...build.witnesses.referenced,
|
|
3366
|
+
funding.collateralUtxo
|
|
3367
|
+
])
|
|
3368
|
+
}, chain, {
|
|
3369
|
+
record: context.record,
|
|
3370
|
+
control: {
|
|
3371
|
+
state: effect.state,
|
|
3372
|
+
lovelace: paid.controlLovelace
|
|
3373
|
+
},
|
|
3374
|
+
issuedGrants: issued ? [{
|
|
3375
|
+
grant: issued.grant,
|
|
3376
|
+
lovelace: issued.output.value.coins
|
|
3377
|
+
}] : [],
|
|
3378
|
+
sweptSlots: swept.map(({ grant }) => grant.slot),
|
|
3379
|
+
signer,
|
|
3380
|
+
deviceKeyHash: signer,
|
|
3381
|
+
outflow: {
|
|
3382
|
+
requested: totalValueOf(effect.outputs),
|
|
3383
|
+
withdrawal: withdrawnOf(build)
|
|
3384
|
+
},
|
|
3385
|
+
funding
|
|
3386
|
+
});
|
|
3387
|
+
};
|
|
3388
|
+
//#endregion
|
|
3389
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/build-custody-transaction.ts
|
|
3390
|
+
const ACCOUNT_OPERATION_TYPES = new Set([
|
|
3391
|
+
"add-device",
|
|
3392
|
+
"delegate",
|
|
3393
|
+
"grant-spend",
|
|
3394
|
+
"issue-grant",
|
|
3395
|
+
"owner-spend",
|
|
3396
|
+
"remove-device",
|
|
3397
|
+
"replace-grant",
|
|
3398
|
+
"revoke-all-grants",
|
|
3399
|
+
"revoke-grant",
|
|
3400
|
+
"sweep-grants",
|
|
3401
|
+
"withdraw-rewards"
|
|
3402
|
+
]);
|
|
3403
|
+
const refuseOperation = (type) => failCustodyBuild({
|
|
3404
|
+
code: "invalid-operation",
|
|
3405
|
+
reason: "unknown-operation",
|
|
3406
|
+
message: `Unknown custody operation ${type} for this request`
|
|
3407
|
+
});
|
|
3408
|
+
const refuseFundingMode = (mode) => failCustodyBuild({
|
|
3409
|
+
code: "invalid-operation",
|
|
3410
|
+
reason: "funding-mode",
|
|
3411
|
+
message: `Funding mode ${mode} does not fit the operation`
|
|
3412
|
+
});
|
|
3413
|
+
/**
|
|
3414
|
+
* Builds the custody transaction a request asks for. Creation runs in the
|
|
3415
|
+
* sponsor's fee mode on the current network. Every operation on an
|
|
3416
|
+
* existing account runs in collateral mode, with the account paying: the
|
|
3417
|
+
* sponsor's fee mode serves creation only.
|
|
3418
|
+
*/
|
|
3419
|
+
const buildCustodyTransaction = async (request, sources) => {
|
|
3420
|
+
const chain = await loadCustodyChain(sources);
|
|
3421
|
+
if (!("accountId" in request)) {
|
|
3422
|
+
const { operation, funding } = request;
|
|
3423
|
+
if (operation.type !== "create") return refuseOperation(operation.type);
|
|
3424
|
+
if (funding.mode !== "fee") return refuseFundingMode(funding.mode);
|
|
3425
|
+
return buildCustodyCreation(operation, funding, chain);
|
|
3426
|
+
}
|
|
3427
|
+
const { accountId, operation, funding } = request;
|
|
3428
|
+
if (!ACCOUNT_OPERATION_TYPES.has(operation.type)) return refuseOperation(operation.type);
|
|
3429
|
+
if (funding.mode !== "collateral") return refuseFundingMode(funding.mode);
|
|
3430
|
+
const context = await resolveCustodyAccount(accountId, await sources.snapshot(accountId), chain);
|
|
3431
|
+
return operation.type === "grant-spend" ? buildCustodyGrantSpend({
|
|
3432
|
+
operation,
|
|
3433
|
+
funding
|
|
3434
|
+
}, context, chain) : buildCustodyOwnerOperation({
|
|
3435
|
+
operation,
|
|
3436
|
+
funding
|
|
3437
|
+
}, context, chain);
|
|
3438
|
+
};
|
|
3439
|
+
//#endregion
|
|
3440
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/creator-index-probe-side-effect.ts
|
|
3441
|
+
const isSoftIndex = (index) => Number.isSafeInteger(index) && index >= 0 && index < 2147483648;
|
|
3442
|
+
const probeErrorOf = (reason) => {
|
|
3443
|
+
if (reason.code === "unavailable" || reason.code === "build-failed") return reason;
|
|
3444
|
+
if (reason.code === "invalid-operation" && reason.reason === "invalid-device-key") return {
|
|
3445
|
+
...reason,
|
|
3446
|
+
reason: reason.reason
|
|
3447
|
+
};
|
|
3448
|
+
return {
|
|
3449
|
+
code: "build-failed",
|
|
3450
|
+
message: `Unexpected creator index probe failure: ${reason.code}`
|
|
3451
|
+
};
|
|
3452
|
+
};
|
|
3453
|
+
const resultOf = ({ accountValidatorHash, creatorIndex }, state) => {
|
|
3454
|
+
const probed = {
|
|
3455
|
+
accountValidatorHash,
|
|
3456
|
+
creatorIndex
|
|
3457
|
+
};
|
|
3458
|
+
return state.status === "account" ? {
|
|
3459
|
+
...probed,
|
|
3460
|
+
status: "account",
|
|
3461
|
+
record: state.record
|
|
3462
|
+
} : {
|
|
3463
|
+
...probed,
|
|
3464
|
+
status: state.status
|
|
3465
|
+
};
|
|
3466
|
+
};
|
|
3467
|
+
const probe = async (request, { chainId, provider, logger }) => {
|
|
3468
|
+
const contractVersion = require_hex.findCustodyContractVersion(request.accountValidatorHash);
|
|
3469
|
+
if (!contractVersion.ok) return {
|
|
3470
|
+
ok: false,
|
|
3471
|
+
error: contractVersion.error
|
|
3472
|
+
};
|
|
3473
|
+
if (!isSoftIndex(request.creatorIndex)) return {
|
|
3474
|
+
ok: false,
|
|
3475
|
+
error: {
|
|
3476
|
+
code: "invalid-operation",
|
|
3477
|
+
reason: "invalid-start-index",
|
|
3478
|
+
message: `Invalid creator index ${request.creatorIndex}`
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
3481
|
+
if (!chainId) return {
|
|
3482
|
+
ok: false,
|
|
3483
|
+
error: {
|
|
3484
|
+
code: "unavailable",
|
|
3485
|
+
message: "Cardano chain id not available"
|
|
3486
|
+
}
|
|
3487
|
+
};
|
|
3488
|
+
try {
|
|
3489
|
+
return {
|
|
3490
|
+
ok: true,
|
|
3491
|
+
result: resultOf(request, await readCustodyCreatorIndex({
|
|
3492
|
+
contractVersion: contractVersion.value,
|
|
3493
|
+
deviceAccountPublicKey: request.deviceAccountPublicKey,
|
|
3494
|
+
creatorIndex: request.creatorIndex
|
|
3495
|
+
}, {
|
|
3496
|
+
chainId,
|
|
3497
|
+
provider
|
|
3498
|
+
}))
|
|
3499
|
+
};
|
|
3500
|
+
} catch (error) {
|
|
3501
|
+
if (error instanceof CustodyBuildFailure) return {
|
|
3502
|
+
ok: false,
|
|
3503
|
+
error: probeErrorOf(error.reason)
|
|
3504
|
+
};
|
|
3505
|
+
logger.error("Custody creator index probe failed unexpectedly", error);
|
|
3506
|
+
return {
|
|
3507
|
+
ok: false,
|
|
3508
|
+
error: {
|
|
3509
|
+
code: "build-failed",
|
|
3510
|
+
message: messageOf(error)
|
|
3511
|
+
}
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
};
|
|
3515
|
+
const makeCustodyCreatorIndexProbing = () => ({ cardanoCustody: { probeCustodyCreatorIndex$ } }, { cardanoContext }, { cardanoProvider, actions, logger }) => {
|
|
3516
|
+
const answer = async (request) => probe(request, {
|
|
3517
|
+
chainId: await (0, rxjs.firstValueFrom)(cardanoContext.selectChainId$),
|
|
3518
|
+
provider: cardanoProvider,
|
|
3519
|
+
logger
|
|
3520
|
+
});
|
|
3521
|
+
return probeCustodyCreatorIndex$.pipe((0, rxjs.mergeMap)(({ payload: request }) => (0, rxjs.from)(answer(request)).pipe((0, rxjs.map)((outcome) => outcome.ok ? actions.cardanoCustody.probeCustodyCreatorIndexCompleted({
|
|
3522
|
+
requestId: request.requestId,
|
|
3523
|
+
result: outcome.result
|
|
3524
|
+
}) : actions.cardanoCustody.probeCustodyCreatorIndexFailed({
|
|
3525
|
+
requestId: request.requestId,
|
|
3526
|
+
error: outcome.error
|
|
3527
|
+
})))));
|
|
3528
|
+
};
|
|
3529
|
+
//#endregion
|
|
3530
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/build-side-effect.ts
|
|
3531
|
+
const failureOf = (error, logger) => {
|
|
3532
|
+
if (error instanceof CustodyBuildFailure) return error.reason;
|
|
3533
|
+
logger.error("Custody transaction build failed unexpectedly", error);
|
|
3534
|
+
return {
|
|
3535
|
+
code: "build-failed",
|
|
3536
|
+
message: messageOf(error)
|
|
3537
|
+
};
|
|
3538
|
+
};
|
|
3539
|
+
const makeCustodyTransactionBuilding = () => ({ cardanoCustody: { buildCustodyTransaction$ } }, { cardanoContext, cardanoCustody, wallets }, { cardanoProvider, actions, logger }) => {
|
|
3540
|
+
const snapshot = async (accountId) => {
|
|
3541
|
+
const [allWallets, accountStateOf, settledUtxos] = await Promise.all([
|
|
3542
|
+
(0, rxjs.firstValueFrom)(wallets.selectAll$),
|
|
3543
|
+
(0, rxjs.firstValueFrom)(cardanoCustody.selectCardanoCustodyAccountState$),
|
|
3544
|
+
(0, rxjs.firstValueFrom)(cardanoContext.selectAccountUtxos$)
|
|
3545
|
+
]);
|
|
3546
|
+
return {
|
|
3547
|
+
account: allWallets.flatMap((wallet) => wallet.accounts).filter(require_account.isCardanoCustodyAccount).find((account) => account.accountId === accountId),
|
|
3548
|
+
accountState: accountStateOf(accountId),
|
|
3549
|
+
settledUtxos: settledUtxos[accountId] ?? []
|
|
3550
|
+
};
|
|
3551
|
+
};
|
|
3552
|
+
const build = async (request) => {
|
|
3553
|
+
try {
|
|
3554
|
+
const [chainId, eraSummaries] = await Promise.all([(0, rxjs.firstValueFrom)(cardanoContext.selectChainId$), (0, rxjs.firstValueFrom)(cardanoContext.selectEraSummaries$)]);
|
|
3555
|
+
return {
|
|
3556
|
+
ok: true,
|
|
3557
|
+
result: await buildCustodyTransaction(require_serializable.Serializable.from(request), {
|
|
3558
|
+
chainId,
|
|
3559
|
+
eraSummaries,
|
|
3560
|
+
snapshot,
|
|
3561
|
+
provider: cardanoProvider,
|
|
3562
|
+
now: Date.now()
|
|
3563
|
+
})
|
|
3564
|
+
};
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
return {
|
|
3567
|
+
ok: false,
|
|
3568
|
+
error: failureOf(error, logger)
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
return buildCustodyTransaction$.pipe((0, rxjs.mergeMap)(({ payload: { requestId, request } }) => (0, rxjs.from)(build(request)).pipe((0, rxjs.map)((outcome) => outcome.ok ? actions.cardanoCustody.buildCustodyTransactionCompleted({
|
|
3573
|
+
requestId,
|
|
3574
|
+
result: outcome.result
|
|
3575
|
+
}) : actions.cardanoCustody.buildCustodyTransactionFailed({
|
|
3576
|
+
requestId,
|
|
3577
|
+
error: outcome.error
|
|
3578
|
+
})))));
|
|
3579
|
+
};
|
|
3580
|
+
const custodyTransactionSideEffects = [makeCustodyTransactionBuilding(), makeCustodyCreatorIndexProbing()];
|
|
3581
|
+
//#endregion
|
|
3582
|
+
//#region ../../packages/module/blockchain-cardano/src/store/custody/init.ts
|
|
3583
|
+
const init = () => ({ sideEffects: custodyTransactionSideEffects });
|
|
3584
|
+
//#endregion
|
|
3585
|
+
exports.default = init;
|