diff --git a/package.json b/package.json index 00168f8..e75cb28 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "private": true, "repository": { "type": "git", - "url": "https://github.com/ahangsu/vscode-teal-debug.git" + "url": "https://github.com/algorand/teal-debugger.git" }, "bugs": { - "url": "https://github.com/ahangsu/vscode-teal-debug/issues" + "url": "https://github.com/algorand/teal-debugger/issues" }, "scripts": { "compile": "tsc -p ./", diff --git a/sampleWorkspace/errors/app/fail.teal b/sampleWorkspace/errors/app/fail.teal new file mode 100644 index 0000000..dc95818 --- /dev/null +++ b/sampleWorkspace/errors/app/fail.teal @@ -0,0 +1,3 @@ +#pragma version 9 +int 0 +return diff --git a/sampleWorkspace/errors/app/fail.teal.tok.map b/sampleWorkspace/errors/app/fail.teal.tok.map new file mode 100644 index 0000000..29ef4f4 --- /dev/null +++ b/sampleWorkspace/errors/app/fail.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["fail.teal"],"names":[],"mappings":";AACA;;AACA"} \ No newline at end of file diff --git a/sampleWorkspace/errors/app/lsig.teal b/sampleWorkspace/errors/app/lsig.teal new file mode 100644 index 0000000..6fc4b6b --- /dev/null +++ b/sampleWorkspace/errors/app/lsig.teal @@ -0,0 +1,7 @@ +#pragma version 9 +txn Fee +! +txn RekeyTo +global ZeroAddress +== +&& diff --git a/sampleWorkspace/errors/app/lsig.teal.tok.map b/sampleWorkspace/errors/app/lsig.teal.tok.map new file mode 100644 index 0000000..63cdf07 --- /dev/null +++ b/sampleWorkspace/errors/app/lsig.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["lsig.teal"],"names":[],"mappings":";AACA;;AACA;AACA;;AACA;;AACA;AACA"} \ No newline at end of file diff --git a/sampleWorkspace/errors/app/simulate-response.json b/sampleWorkspace/errors/app/simulate-response.json new file mode 100644 index 0000000..f89b50a --- /dev/null +++ b/sampleWorkspace/errors/app/simulate-response.json @@ -0,0 +1,151 @@ +{ + "exec-trace-config": { + "enable": true, + "scratch-change": true, + "stack-change": true, + "state-change": true + }, + "initial-states": {}, + "last-round": 6, + "txn-groups": [ + { + "app-budget-added": 700, + "app-budget-consumed": 2, + "failed-at": [ + 1 + ], + "failure-message": "transaction Z4XYNPFASUB3NMVQQUVY3MM74IMSZTJKWSWTTUIBX5GZZNIS2ZEA: transaction rejected by ApprovalProgram", + "txn-results": [ + { + "exec-trace": { + "logic-sig-hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=", + "logic-sig-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2 + } + ] + }, + { + "pc": 3, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 1 + }, + { + "pc": 4, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 6, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 8, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + }, + { + "pc": 9, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + } + ] + }, + "logic-sig-budget-consumed": 6, + "txn-result": { + "pool-error": "", + "txn": { + "lsig": { + "l": "CTEBFDEgMgMSEA==" + }, + "txn": { + "amt": 12345, + "fv": 5, + "gen": "sandnet-v1", + "gh": "DWJD/xU1XdpYdgZoVDTESyDLE/AVAtmysydsjSQagX4=", + "grp": "iYqBpg5CrgoI+h0C31SMHwkgOZcmBF80YgP5SjQWFaI=", + "lv": 1005, + "note": "o7BM5SRJ9zA=", + "rcv": "5AI27VDGCCIDV425ZUJPKDCTP3R5P2T6VFJYZV4C4NBWII3INIQ6IKHMCU", + "snd": "KCAUHNOBMNSUBLLGPD3HUZFKG7SUYKY7JSCZ3KV6WVP3FMKEPQOYS3KG5Y", + "type": "pay" + } + } + } + }, + { + "app-budget-consumed": 2, + "exec-trace": { + "approval-program-hash": "EsgA+UYADvM+Xgw5LdVrJGIRxHi4DX1+Or5rtns9jiQ=", + "approval-program-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2 + } + ] + }, + { + "pc": 3, + "stack-additions": [ + { + "type": 2 + } + ], + "stack-pop-count": 1 + } + ] + }, + "txn-result": { + "application-index": 1008, + "pool-error": "", + "txn": { + "sig": "RvhYC4Ce5HCrnP7RyKWHcBtNa+ufusAc0fu2B/BKDBtIKGQPEv2DgyD7PLUT95GbuFz2p9Zwyx7HYYDn+lI/Cw==", + "txn": { + "apap": "CYEAQw==", + "apsu": "CYEB", + "fee": 2000, + "fv": 5, + "gh": "DWJD/xU1XdpYdgZoVDTESyDLE/AVAtmysydsjSQagX4=", + "grp": "iYqBpg5CrgoI+h0C31SMHwkgOZcmBF80YgP5SjQWFaI=", + "lv": 1005, + "note": "KiQF0SR0bz4=", + "snd": "THBZEHQO2AAT2BEO4WVDOSYW77TQRPQQZ2IU3PLJVT6Z3QCNJJJOT76KGA", + "type": "appl" + } + } + } + } + ] + } + ], + "version": 2 +} diff --git a/sampleWorkspace/errors/app/sources.json b/sampleWorkspace/errors/app/sources.json new file mode 100644 index 0000000..98b9ce0 --- /dev/null +++ b/sampleWorkspace/errors/app/sources.json @@ -0,0 +1,12 @@ +{ + "txn-group-sources": [ + { + "sourcemap-location": "./lsig.teal.tok.map", + "hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=" + }, + { + "sourcemap-location": "./fail.teal.tok.map", + "hash": "EsgA+UYADvM+Xgw5LdVrJGIRxHi4DX1+Or5rtns9jiQ=" + } + ] +} diff --git a/sampleWorkspace/errors/inner-app/app-reject-simulate-response.json b/sampleWorkspace/errors/inner-app/app-reject-simulate-response.json new file mode 100644 index 0000000..506381a --- /dev/null +++ b/sampleWorkspace/errors/inner-app/app-reject-simulate-response.json @@ -0,0 +1,175 @@ +{ + "exec-trace-config": { + "enable": true, + "scratch-change": true, + "stack-change": true, + "state-change": true + }, + "initial-states": {}, + "last-round": 12, + "txn-groups": [ + { + "app-budget-added": 1400, + "app-budget-consumed": 15, + "failed-at": [ + 0, + 0 + ], + "failure-message": "transaction 2APGJLEQVGE3ZVHPR6UNSFWAW7BGYYVKVCFENWX4CBZUM5CQX7EQ: logic eval error: logic eval error: assert failed pc=10. Details: pc=10, opcodes=txna ApplicationArgs 0; btoi; assert; label1:. Details: pc=21, opcodes=txna ApplicationArgs 0; itxn_field ApplicationArgs; itxn_submit; label1:", + "txn-results": [ + { + "app-budget-consumed": 15, + "exec-trace": { + "approval-program-hash": "qZ6vI+Hx+bhXlZbOykVWCWJT/4tSYXBygoQBHH7xIlQ=", + "approval-program-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2, + "uint": 1011 + } + ] + }, + { + "pc": 3, + "stack-pop-count": 1 + }, + { + "pc": 6 + }, + { + "pc": 7, + "stack-additions": [ + { + "type": 2, + "uint": 6 + } + ] + }, + { + "pc": 9, + "stack-pop-count": 1 + }, + { + "pc": 11, + "stack-additions": [ + { + "type": 2, + "uint": 1007 + } + ] + }, + { + "pc": 14, + "stack-pop-count": 1 + }, + { + "pc": 16, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 19, + "stack-pop-count": 1 + }, + { + "pc": 21, + "spawned-inners": [ + 0 + ] + } + ], + "inner-trace": [ + { + "approval-program-hash": "1KZi4myBA+q7zZAoHAPPiX/zBQxj8QbO7u1dWJMud4M=", + "approval-program-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2, + "uint": 1007 + } + ] + }, + { + "pc": 3, + "stack-pop-count": 1 + }, + { + "pc": 6, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 9, + "stack-additions": [ + { + "type": 2 + } + ], + "stack-pop-count": 1 + }, + { + "pc": 10, + "stack-pop-count": 1 + } + ] + } + ] + }, + "txn-result": { + "inner-txns": [ + { + "pool-error": "", + "txn": { + "txn": { + "apaa": [ + "AAAAAAAAAAA=" + ], + "apid": 1007, + "fee": 1000, + "fv": 11, + "lv": 1011, + "snd": "YCVGIALPB5UCXY3QS4ICXLGXXAAFCA35QAEUB2MSBH7J6UJQU5D3LEY36Q", + "type": "appl" + } + } + } + ], + "pool-error": "", + "txn": { + "sig": "qGI2+ooLqAU0+5jUDhNUzneSQrq+WoB6lPmezTmiUTASkCwR/ttI53flPOqUDoOjnoyEAVpWN8L9jemrL9ybBA==", + "txn": { + "apaa": [ + "AAAAAAAAAAA=" + ], + "apfa": [ + 1007 + ], + "apid": 1011, + "fee": 1000, + "fv": 11, + "gh": "DWJD/xU1XdpYdgZoVDTESyDLE/AVAtmysydsjSQagX4=", + "lv": 1011, + "note": "h2tFkRbRvJk=", + "snd": "THBZEHQO2AAT2BEO4WVDOSYW77TQRPQQZ2IU3PLJVT6Z3QCNJJJOT76KGA", + "type": "appl" + } + } + } + } + ] + } + ], + "version": 2 +} diff --git a/sampleWorkspace/errors/inner-app/inner.teal b/sampleWorkspace/errors/inner-app/inner.teal new file mode 100644 index 0000000..182c31d --- /dev/null +++ b/sampleWorkspace/errors/inner-app/inner.teal @@ -0,0 +1,10 @@ +#pragma version 9 +txn ApplicationID +bz end + +txn ApplicationArgs 0 +btoi +assert + +end: +int 1 diff --git a/sampleWorkspace/errors/inner-app/inner.teal.tok.map b/sampleWorkspace/errors/inner-app/inner.teal.tok.map new file mode 100644 index 0000000..934fb0b --- /dev/null +++ b/sampleWorkspace/errors/inner-app/inner.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["inner.teal"],"names":[],"mappings":";AACA;;AACA;;;AAEA;;;AACA;AACA;AAGA"} \ No newline at end of file diff --git a/sampleWorkspace/errors/inner-app/outer.teal b/sampleWorkspace/errors/inner-app/outer.teal new file mode 100644 index 0000000..555d134 --- /dev/null +++ b/sampleWorkspace/errors/inner-app/outer.teal @@ -0,0 +1,15 @@ +#pragma version 9 +txn ApplicationID +bz end + +itxn_begin +int appl +itxn_field TypeEnum +txn Applications 1 +itxn_field ApplicationID +txn ApplicationArgs 0 +itxn_field ApplicationArgs +itxn_submit + +end: +int 1 diff --git a/sampleWorkspace/errors/inner-app/outer.teal.tok.map b/sampleWorkspace/errors/inner-app/outer.teal.tok.map new file mode 100644 index 0000000..8f6a107 --- /dev/null +++ b/sampleWorkspace/errors/inner-app/outer.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["outer.teal"],"names":[],"mappings":";AACA;;AACA;;;AAEA;AACA;;AACA;;AACA;;;AACA;;AACA;;;AACA;;AACA;AAGA"} \ No newline at end of file diff --git a/sampleWorkspace/errors/inner-app/overspend-simulate-response.json b/sampleWorkspace/errors/inner-app/overspend-simulate-response.json new file mode 100644 index 0000000..f22a684 --- /dev/null +++ b/sampleWorkspace/errors/inner-app/overspend-simulate-response.json @@ -0,0 +1,136 @@ +{ + "exec-trace-config": { + "enable": true, + "scratch-change": true, + "stack-change": true, + "state-change": true + }, + "initial-states": {}, + "last-round": 11, + "txn-groups": [ + { + "app-budget-added": 1400, + "app-budget-consumed": 10, + "failed-at": [ + 0, + 0 + ], + "failure-message": "transaction 2APGJLEQVGE3ZVHPR6UNSFWAW7BGYYVKVCFENWX4CBZUM5CQX7EQ: logic eval error: overspend (account YCVGIALPB5UCXY3QS4ICXLGXXAAFCA35QAEUB2MSBH7J6UJQU5D3LEY36Q, data {AccountBaseData:{Status:Offline MicroAlgos:{Raw:0} RewardsBase:0 RewardedMicroAlgos:{Raw:0} AuthAddr:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ TotalAppSchema:{_struct:{} NumUint:0 NumByteSlice:0} TotalExtraAppPages:0 TotalAppParams:0 TotalAppLocalStates:0 TotalAssetParams:0 TotalAssets:0 TotalBoxes:0 TotalBoxBytes:0} VotingData:{VoteID:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] SelectionID:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] StateProofID:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] VoteFirstValid:0 VoteLastValid:0 VoteKeyDilution:0}}, tried to spend {1000}). Details: pc=21, opcodes=txna ApplicationArgs 0; itxn_field ApplicationArgs; itxn_submit; label1:", + "txn-results": [ + { + "app-budget-consumed": 10, + "exec-trace": { + "approval-program-hash": "qZ6vI+Hx+bhXlZbOykVWCWJT/4tSYXBygoQBHH7xIlQ=", + "approval-program-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2, + "uint": 1011 + } + ] + }, + { + "pc": 3, + "stack-pop-count": 1 + }, + { + "pc": 6 + }, + { + "pc": 7, + "stack-additions": [ + { + "type": 2, + "uint": 6 + } + ] + }, + { + "pc": 9, + "stack-pop-count": 1 + }, + { + "pc": 11, + "stack-additions": [ + { + "type": 2, + "uint": 1007 + } + ] + }, + { + "pc": 14, + "stack-pop-count": 1 + }, + { + "pc": 16, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 19, + "stack-pop-count": 1 + }, + { + "pc": 21, + "spawned-inners": [ + 0 + ] + } + ], + "inner-trace": [ + {} + ] + }, + "txn-result": { + "inner-txns": [ + { + "pool-error": "", + "txn": { + "txn": { + "apaa": [ + "AAAAAAAAAAA=" + ], + "apid": 1007, + "fee": 1000, + "fv": 11, + "lv": 1011, + "snd": "YCVGIALPB5UCXY3QS4ICXLGXXAAFCA35QAEUB2MSBH7J6UJQU5D3LEY36Q", + "type": "appl" + } + } + } + ], + "pool-error": "", + "txn": { + "sig": "qGI2+ooLqAU0+5jUDhNUzneSQrq+WoB6lPmezTmiUTASkCwR/ttI53flPOqUDoOjnoyEAVpWN8L9jemrL9ybBA==", + "txn": { + "apaa": [ + "AAAAAAAAAAA=" + ], + "apfa": [ + 1007 + ], + "apid": 1011, + "fee": 1000, + "fv": 11, + "gh": "DWJD/xU1XdpYdgZoVDTESyDLE/AVAtmysydsjSQagX4=", + "lv": 1011, + "note": "h2tFkRbRvJk=", + "snd": "THBZEHQO2AAT2BEO4WVDOSYW77TQRPQQZ2IU3PLJVT6Z3QCNJJJOT76KGA", + "type": "appl" + } + } + } + } + ] + } + ], + "version": 2 +} diff --git a/sampleWorkspace/errors/inner-app/sources.json b/sampleWorkspace/errors/inner-app/sources.json new file mode 100644 index 0000000..035ab16 --- /dev/null +++ b/sampleWorkspace/errors/inner-app/sources.json @@ -0,0 +1,12 @@ +{ + "txn-group-sources": [ + { + "sourcemap-location": "./outer.teal.tok.map", + "hash": "qZ6vI+Hx+bhXlZbOykVWCWJT/4tSYXBygoQBHH7xIlQ=" + }, + { + "sourcemap-location": "./inner.teal.tok.map", + "hash": "1KZi4myBA+q7zZAoHAPPiX/zBQxj8QbO7u1dWJMud4M=" + } + ] +} diff --git a/sampleWorkspace/errors/logicsig/lsig-err.teal b/sampleWorkspace/errors/logicsig/lsig-err.teal new file mode 100644 index 0000000..7ada0d3 --- /dev/null +++ b/sampleWorkspace/errors/logicsig/lsig-err.teal @@ -0,0 +1,8 @@ +#pragma version 9 +txn Fee +! +err +txn RekeyTo +global ZeroAddress +== +&& diff --git a/sampleWorkspace/errors/logicsig/lsig-err.teal.tok.map b/sampleWorkspace/errors/logicsig/lsig-err.teal.tok.map new file mode 100644 index 0000000..d8e97ff --- /dev/null +++ b/sampleWorkspace/errors/logicsig/lsig-err.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["lsig-err.teal"],"names":[],"mappings":";AACA;;AACA;AACA;AACA;;AACA;;AACA;AACA"} \ No newline at end of file diff --git a/sampleWorkspace/errors/logicsig/lsig.teal b/sampleWorkspace/errors/logicsig/lsig.teal new file mode 100644 index 0000000..6fc4b6b --- /dev/null +++ b/sampleWorkspace/errors/logicsig/lsig.teal @@ -0,0 +1,7 @@ +#pragma version 9 +txn Fee +! +txn RekeyTo +global ZeroAddress +== +&& diff --git a/sampleWorkspace/errors/logicsig/lsig.teal.tok.map b/sampleWorkspace/errors/logicsig/lsig.teal.tok.map new file mode 100644 index 0000000..63cdf07 --- /dev/null +++ b/sampleWorkspace/errors/logicsig/lsig.teal.tok.map @@ -0,0 +1 @@ +{"version":3,"sources":["lsig.teal"],"names":[],"mappings":";AACA;;AACA;AACA;;AACA;;AACA;AACA"} \ No newline at end of file diff --git a/sampleWorkspace/errors/logicsig/simulate-response.json b/sampleWorkspace/errors/logicsig/simulate-response.json new file mode 100644 index 0000000..538f89d --- /dev/null +++ b/sampleWorkspace/errors/logicsig/simulate-response.json @@ -0,0 +1,155 @@ +{ + "exec-trace-config": { + "enable": true, + "scratch-change": true, + "stack-change": true, + "state-change": true + }, + "initial-states": {}, + "last-round": 5, + "txn-groups": [ + { + "failed-at": [ + 1 + ], + "failure-message": "transaction {_struct:{} Sig:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] Msig:{_struct:{} Version:0 Threshold:0 Subsigs:[]} Lsig:{_struct:{} Logic:[9 49 1 20 0 49 32 50 3 18 16] Sig:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] Msig:{_struct:{} Version:0 Threshold:0 Subsigs:[]} Args:[]} Txn:{_struct:{} Type:pay Header:{_struct:{} Sender:NYWSPJOL42GPOSJL5CHLLMTF2OI65DK45BNODHB2AZDYJ6AS4F73VDTYVU Fee:{Raw:2000} FirstValid:5 LastValid:1005 Note:[1 144 175 4 138 220 202 59] GenesisID:sandnet-v1 GenesisHash:BVREH7YVGVO5UWDWAZUFINGEJMQMWE7QCUBNTMVTE5WI2JA2QF7A Group:RHWBU7AW2AA52WP55GM3UYK22BGA4XR3XJQQTSZDWZSUWW74VFYQ Lease:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] RekeyTo:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ} KeyregTxnFields:{_struct:{} VotePK:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] SelectionPK:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] StateProofPK:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] VoteFirst:0 VoteLast:0 VoteKeyDilution:0 Nonparticipation:false} PaymentTxnFields:{_struct:{} Receiver:5AI27VDGCCIDV425ZUJPKDCTP3R5P2T6VFJYZV4C4NBWII3INIQ6IKHMCU Amount:{Raw:12345} CloseRemainderTo:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ} AssetConfigTxnFields:{_struct:{} ConfigAsset:0 AssetParams:{_struct:{} Total:0 Decimals:0 DefaultFrozen:false UnitName: AssetName: URL: MetadataHash:[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] Manager:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ Reserve:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ Freeze:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ Clawback:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ}} AssetTransferTxnFields:{_struct:{} XferAsset:0 AssetAmount:0 AssetSender:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ AssetReceiver:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ AssetCloseTo:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ} AssetFreezeTxnFields:{_struct:{} FreezeAccount:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ FreezeAsset:0 AssetFrozen:false} ApplicationCallTxnFields:{_struct:{} ApplicationID:0 OnCompletion:NoOpOC ApplicationArgs:[] Accounts:[] ForeignApps:[] Boxes:[] ForeignAssets:[] LocalStateSchema:{_struct:{} NumUint:0 NumByteSlice:0} GlobalStateSchema:{_struct:{} NumUint:0 NumByteSlice:0} ApprovalProgram:[] ClearStateProgram:[] ExtraProgramPages:0} StateProofTxnFields:{_struct:{} StateProofType:0 StateProof:StateProof: {} Message:{_struct:{} BlockHeadersCommitment:[] VotersCommitment:[] LnProvenWeight:0 FirstAttestedRound:0 LastAttestedRound:0}}} AuthAddr:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ} invalid : transaction SDZ7UTMY5TJPLCTIOXHAKGT3HHMATQWC46OKPD6L54Z2RGXPMJOA: rejected by logic err=err opcode executed. Details: pc=4, opcodes=txn Fee; !; err", + "txn-results": [ + { + "exec-trace": { + "logic-sig-hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=", + "logic-sig-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2 + } + ] + }, + { + "pc": 3, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 1 + }, + { + "pc": 4, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 6, + "stack-additions": [ + { + "bytes": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "type": 1 + } + ] + }, + { + "pc": 8, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + }, + { + "pc": 9, + "stack-additions": [ + { + "type": 2, + "uint": 1 + } + ], + "stack-pop-count": 2 + } + ] + }, + "logic-sig-budget-consumed": 6, + "txn-result": { + "pool-error": "", + "txn": { + "lsig": { + "l": "CTEBFDEgMgMSEA==" + }, + "txn": { + "amt": 12345, + "fv": 5, + "gen": "sandnet-v1", + "gh": "DWJD/xU1XdpYdgZoVDTESyDLE/AVAtmysydsjSQagX4=", + "grp": "iewafBbQAd1Z/emZumFa0EwOXju6YQnLI7ZlS1v8qXE=", + "lv": 1005, + "note": "o7BM5SRJ9zA=", + "rcv": "5AI27VDGCCIDV425ZUJPKDCTP3R5P2T6VFJYZV4C4NBWII3INIQ6IKHMCU", + "snd": "KCAUHNOBMNSUBLLGPD3HUZFKG7SUYKY7JSCZ3KV6WVP3FMKEPQOYS3KG5Y", + "type": "pay" + } + } + } + }, + { + "exec-trace": { + "logic-sig-hash": "NW2mJ5d46uPJ1lgbk6qkHJWih9w/XrZtvZu/cWV7b5I=", + "logic-sig-trace": [ + { + "pc": 1, + "stack-additions": [ + { + "type": 2, + "uint": 2000 + } + ] + }, + { + "pc": 3, + "stack-additions": [ + { + "type": 2 + } + ], + "stack-pop-count": 1 + }, + { + "pc": 4 + } + ] + }, + "logic-sig-budget-consumed": 3, + "txn-result": { + "pool-error": "", + "txn": { + "lsig": { + "l": "CTEBFAAxIDIDEhA=" + }, + "txn": { + "amt": 12345, + "fee": 2000, + "fv": 5, + "gen": "sandnet-v1", + "gh": "DWJD/xU1XdpYdgZoVDTESyDLE/AVAtmysydsjSQagX4=", + "grp": "iewafBbQAd1Z/emZumFa0EwOXju6YQnLI7ZlS1v8qXE=", + "lv": 1005, + "note": "AZCvBIrcyjs=", + "rcv": "5AI27VDGCCIDV425ZUJPKDCTP3R5P2T6VFJYZV4C4NBWII3INIQ6IKHMCU", + "snd": "NYWSPJOL42GPOSJL5CHLLMTF2OI65DK45BNODHB2AZDYJ6AS4F73VDTYVU", + "type": "pay" + } + } + } + } + ] + } + ], + "version": 2 +} diff --git a/sampleWorkspace/errors/logicsig/sources.json b/sampleWorkspace/errors/logicsig/sources.json new file mode 100644 index 0000000..f30ac0a --- /dev/null +++ b/sampleWorkspace/errors/logicsig/sources.json @@ -0,0 +1,12 @@ +{ + "txn-group-sources": [ + { + "sourcemap-location": "./lsig.teal.tok.map", + "hash": "Inf40LWZTDzZhoxEhTIxBG0nbIxUrg9AQXCrK78bInk=" + }, + { + "sourcemap-location": "./lsig-err.teal.tok.map", + "hash": "NW2mJ5d46uPJ1lgbk6qkHJWih9w/XrZtvZu/cWV7b5I=" + } + ] +} diff --git a/src/debugAdapter/avmRuntime.ts b/src/debugAdapter/avmRuntime.ts index c1f486b..27b4ff9 100644 --- a/src/debugAdapter/avmRuntime.ts +++ b/src/debugAdapter/avmRuntime.ts @@ -3,6 +3,7 @@ import { RuntimeEvents } from './debugRequestHandlers'; import { AppState } from './appState'; import { FrameSourceLocation, + SteppingResultType, TraceReplayEngine, TraceReplayStackFrame, } from './traceReplayEngine'; @@ -100,14 +101,30 @@ export class AvmRuntime extends EventEmitter { private updateCurrentLine(reverse: boolean): boolean { if (reverse) { - if (!this.engine.backward()) { - this.sendEvent(RuntimeEvents.stopOnEntry); - return false; + const result = this.engine.backward(); + switch (result.type) { + case SteppingResultType.END: + this.sendEvent(RuntimeEvents.stopOnEntry); + return false; + case SteppingResultType.EXCEPTION: + this.sendEvent( + RuntimeEvents.stopOnException, + result.exceptionInfo?.message, + ); + return false; } } else { - if (!this.engine.forward()) { - this.sendEvent(RuntimeEvents.end); - return false; + const result = this.engine.forward(); + switch (result.type) { + case SteppingResultType.END: + this.sendEvent(RuntimeEvents.end); + return false; + case SteppingResultType.EXCEPTION: + this.sendEvent( + RuntimeEvents.stopOnException, + result.exceptionInfo?.message, + ); + return false; } } diff --git a/src/debugAdapter/debugRequestHandlers.ts b/src/debugAdapter/debugRequestHandlers.ts index 756170b..742e1ea 100644 --- a/src/debugAdapter/debugRequestHandlers.ts +++ b/src/debugAdapter/debugRequestHandlers.ts @@ -31,6 +31,7 @@ export enum RuntimeEvents { stopOnEntry = 'stopOnEntry', stopOnStep = 'stopOnStep', stopOnBreakpoint = 'stopOnBreakpoint', + stopOnException = 'stopOnException', breakpointValidated = 'breakpointValidated', end = 'end', error = 'error', @@ -100,6 +101,11 @@ export class AvmDebugSession extends DebugSession { this._runtime.on(RuntimeEvents.stopOnBreakpoint, () => { this.sendEvent(new StoppedEvent('breakpoint', AvmDebugSession.threadID)); }); + this._runtime.on(RuntimeEvents.stopOnException, (message) => { + this.sendEvent( + new StoppedEvent('exception', AvmDebugSession.threadID, message), + ); + }); this._runtime.on( RuntimeEvents.breakpointValidated, (bp: IRuntimeBreakpoint) => { diff --git a/src/debugAdapter/traceReplayEngine.ts b/src/debugAdapter/traceReplayEngine.ts index a6b0565..a85daf3 100644 --- a/src/debugAdapter/traceReplayEngine.ts +++ b/src/debugAdapter/traceReplayEngine.ts @@ -7,6 +7,37 @@ import { ProgramSourceDescriptorRegistry, } from './utils'; +export enum SteppingResultType { + /* eslint-disable @typescript-eslint/naming-convention */ + OK, + END, + EXCEPTION, + /* eslint-enable @typescript-eslint/naming-convention */ +} + +export class SteppingResult { + private constructor( + public readonly type: SteppingResultType, + public readonly exceptionInfo?: ExceptionInfo, + ) {} + + public static ok(): SteppingResult { + return new SteppingResult(SteppingResultType.OK); + } + + public static end(): SteppingResult { + return new SteppingResult(SteppingResultType.END); + } + + public static exception(info: ExceptionInfo): SteppingResult { + return new SteppingResult(SteppingResultType.EXCEPTION, info); + } +} + +export class ExceptionInfo { + constructor(public readonly message: string) {} +} + export class TraceReplayEngine { public simulateResponse: algosdk.modelsv2.SimulateResponse | undefined; @@ -153,29 +184,34 @@ export class TraceReplayEngine { return this.stack[this.stack.length - 1]; } - public forward(): boolean { + public forward(): SteppingResult { let length: number; do { length = this.stack.length; - this.currentFrame().forward(this.stack); + const exceptionInfo = this.currentFrame().forward(this.stack); + if (exceptionInfo) { + return SteppingResult.exception(exceptionInfo); + } if (this.stack.length === 0) { - return false; + return SteppingResult.end(); } } while (this.stack.length < length); - return true; + return SteppingResult.ok(); } - public backward(): boolean { + public backward(): SteppingResult { let length: number; do { length = this.stack.length; - this.currentFrame().backward(this.stack); + const exceptionInfo = this.currentFrame().backward(this.stack); if (this.stack.length === 0) { this.setStartingStack(this.simulateResponse!); - return false; + return exceptionInfo + ? SteppingResult.exception(exceptionInfo) + : SteppingResult.end(); } } while (this.stack.length < length); - return true; + return SteppingResult.ok(); } } @@ -238,8 +274,10 @@ export abstract class TraceReplayStackFrame { public abstract sourceFile(): FrameSource; public abstract sourceLocation(): FrameSourceLocation; - public abstract forward(stack: TraceReplayStackFrame[]): void; - public abstract backward(stack: TraceReplayStackFrame[]): void; + public abstract forward(stack: TraceReplayStackFrame[]): ExceptionInfo | void; + public abstract backward( + stack: TraceReplayStackFrame[], + ): ExceptionInfo | void; } export class TopLevelTransactionGroupsFrame extends TraceReplayStackFrame { @@ -290,7 +328,7 @@ export class TopLevelTransactionGroupsFrame extends TraceReplayStackFrame { }; } - public forward(stack: TraceReplayStackFrame[]): void { + public forward(stack: TraceReplayStackFrame[]): ExceptionInfo | void { if (!this.txnGroupDone) { stack.push(this.frameForIndex(this.index)); this.txnGroupDone = true; @@ -304,7 +342,7 @@ export class TopLevelTransactionGroupsFrame extends TraceReplayStackFrame { stack.pop(); } - private frameForIndex(index: number): TransactionStackFrame { + private frameForIndex(index: number): TransactionGroupStackFrame { const txnInfos: algosdk.modelsv2.PendingTransactionResponse[] = []; const txnTraces: Array< algosdk.modelsv2.SimulationTransactionExecTrace | undefined @@ -314,16 +352,24 @@ export class TopLevelTransactionGroupsFrame extends TraceReplayStackFrame { txnInfos.push(txnResult); txnTraces.push(execTrace); } - const txnGroupFrame = new TransactionStackFrame( + let failureInfo: TransactionFailureInfo | undefined = undefined; + if (this.response.txnGroups[index].failedAt) { + failureInfo = { + message: this.response.txnGroups[index].failureMessage!, + path: this.response.txnGroups[index].failedAt!.map((n) => Number(n)), + }; + } + const txnGroupFrame = new TransactionGroupStackFrame( this.engine, - [index], + [index, 0], txnInfos, txnTraces, + failureInfo, ); return txnGroupFrame; } - public backward(stack: TraceReplayStackFrame[]): void { + public backward(stack: TraceReplayStackFrame[]): ExceptionInfo | void { if (this.txnGroupDone) { this.txnGroupDone = false; return; @@ -358,21 +404,28 @@ enum ProgramStatus { /* eslint-enable @typescript-eslint/naming-convention */ } -export class TransactionStackFrame extends TraceReplayStackFrame { +interface TransactionFailureInfo { + message: string; + path: number[]; +} + +export class TransactionGroupStackFrame extends TraceReplayStackFrame { private txnIndex: number = 0; private logicSigStatus: ProgramStatus = ProgramStatus.DONE; private appStatus: ProgramStatus = ProgramStatus.DONE; + private onException: boolean = false; private sourceContent: string; private sourceLocations: TransactionSourceLocation[] = []; constructor( engine: TraceReplayEngine, - private readonly txnPath: number[], + private txnPath: number[], private readonly txnInfos: algosdk.modelsv2.PendingTransactionResponse[], private readonly txnTraces: Array< algosdk.modelsv2.SimulationTransactionExecTrace | undefined >, + private readonly failureInfo: TransactionFailureInfo | undefined, ) { super(engine); @@ -450,7 +503,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { } public name(): string { - return `${this.txnPath.length > 1 ? 'inner ' : ''}transaction ${ + return `${this.txnPath.length > 2 ? 'inner ' : ''}transaction ${ this.txnIndex }`; } @@ -458,8 +511,8 @@ export class TransactionStackFrame extends TraceReplayStackFrame { public sourceFile(): FrameSource { return { name: `${ - this.txnPath.length > 1 ? 'inner-' : '' - }transaction-group-${this.txnPath.join('-')}.json`, + this.txnPath.length > 2 ? 'inner-' : '' + }transaction-group-${this.txnPath.slice(0, -1).join('-')}.json`, content: this.sourceContent, contentMimeType: 'application/json', }; @@ -489,9 +542,34 @@ export class TransactionStackFrame extends TraceReplayStackFrame { return frameSourceLocation; } - public forward(stack: TraceReplayStackFrame[]): void { + public forward(stack: TraceReplayStackFrame[]): ExceptionInfo | void { const currentTxnTrace = this.txnTraces[this.txnIndex]; const currentTxnInfo = this.txnInfos[this.txnIndex]; + + let childFailureInfo: TransactionFailureInfo | undefined = undefined; + if ( + this.failureInfo && + pathStartWith(this.failureInfo.path, this.txnPath.slice(1)) + ) { + if (this.failureInfo.path.length === this.txnPath.length - 1) { + if ( + currentTxnTrace && + (currentTxnTrace.logicSigTrace || + currentTxnTrace.approvalProgramTrace || + currentTxnTrace.clearStateProgramTrace) + ) { + // Fail in the trace + childFailureInfo = this.failureInfo; + } else { + // Fail right now + this.onException = true; + return new ExceptionInfo(this.failureInfo.message); + } + } else { + childFailureInfo = this.failureInfo; + } + } + if (this.logicSigStatus === ProgramStatus.NOT_STARTED) { this.logicSigStatus = ProgramStatus.STARTING; return; @@ -505,6 +583,11 @@ export class TransactionStackFrame extends TraceReplayStackFrame { currentTxnTrace.logicSigTrace!, currentTxnTrace, currentTxnInfo, + // Only forward childFailureInfo if the LogicSig is the one that failed. The LogicSig could + // not have failed if we have an app trace. + this.appStatus === ProgramStatus.NOT_STARTED + ? undefined + : childFailureInfo, ); this.logicSigStatus = ProgramStatus.DONE; stack.push(logicSigFrame); @@ -525,6 +608,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { currentTxnTrace.approvalProgramTrace!, currentTxnTrace, currentTxnInfo, + childFailureInfo, ); } else { appFrame = new ProgramStackFrame( @@ -535,6 +619,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { currentTxnTrace.clearStateProgramTrace!, currentTxnTrace, currentTxnInfo, + childFailureInfo, ); } this.appStatus = ProgramStatus.DONE; @@ -543,6 +628,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { } if (this.txnIndex + 1 < this.txnTraces.length) { this.txnIndex++; + this.txnPath[this.txnPath.length - 1]++; const nextTrace = this.txnTraces[this.txnIndex]; if (nextTrace) { this.logicSigStatus = nextTrace.logicSigTrace @@ -561,7 +647,11 @@ export class TransactionStackFrame extends TraceReplayStackFrame { stack.pop(); } - public backward(stack: TraceReplayStackFrame[]): void { + public backward(stack: TraceReplayStackFrame[]): ExceptionInfo | void { + if (this.onException) { + this.onException = false; + return; + } const currentTrace = this.txnTraces[this.txnIndex]; if (currentTrace) { if ( @@ -576,8 +666,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { this.appStatus = ProgramStatus.NOT_STARTED; // Need to unwind the forward call that is implicit when the app program frame // is popped - this.backward(stack); - return; + return this.backward(stack); } } if (currentTrace.logicSigTrace) { @@ -596,6 +685,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { return; } this.txnIndex--; + this.txnPath[this.txnPath.length - 1]--; this.logicSigStatus = ProgramStatus.DONE; this.appStatus = ProgramStatus.DONE; const previousTrace = this.txnTraces[this.txnIndex]; @@ -605,7 +695,7 @@ export class TransactionStackFrame extends TraceReplayStackFrame { previousTrace?.logicSigHash ) { // Need to step back on the app or lsig status - this.backward(stack); + return this.backward(stack); } } } @@ -619,9 +709,9 @@ export interface ProgramState { export class ProgramStackFrame extends TraceReplayStackFrame { private index: number = 0; private handledInnerTxns: boolean = false; - private innerTxnGroupCount: number = 0; private initialAppState: AppState | undefined; private logicSigAddress: string | undefined; + private blockingException: ExceptionInfo | undefined; public state: ProgramState = { pc: 0, stack: [], scratch: new Map() }; @@ -633,6 +723,7 @@ export class ProgramStackFrame extends TraceReplayStackFrame { private readonly programTrace: algosdk.modelsv2.SimulationOpcodeTraceUnit[], private readonly trace: algosdk.modelsv2.SimulationTransactionExecTrace, private readonly txnInfo: algosdk.modelsv2.PendingTransactionResponse, + private readonly failureInfo: TransactionFailureInfo | undefined, ) { super(engine); this.state.pc = Number(programTrace[0].pc); @@ -723,41 +814,65 @@ export class ProgramStackFrame extends TraceReplayStackFrame { }; } - public forward(stack: TraceReplayStackFrame[]): void { + public forward(stack: TraceReplayStackFrame[]): ExceptionInfo | void { + if (this.blockingException) { + return this.blockingException; + } + + if (this.index === this.programTrace.length) { + stack.pop(); + return; + } + const currentUnit = this.programTrace[this.index]; this.processUnit(currentUnit); const spawnedInners = currentUnit.spawnedInners; if (!this.handledInnerTxns && spawnedInners && spawnedInners.length !== 0) { + const spawnedInnerIndexes = spawnedInners.map((i) => Number(i)); const innerGroupInfo: algosdk.modelsv2.PendingTransactionResponse[] = []; const innerTraces: algosdk.modelsv2.SimulationTransactionExecTrace[] = []; - for (const innerIndex of spawnedInners) { - const innerTxnInfo = this.txnInfo.innerTxns![Number(innerIndex)]; - const innerTrace = this.trace.innerTrace![Number(innerIndex)]; + for (const innerIndex of spawnedInnerIndexes) { + const innerTxnInfo = this.txnInfo.innerTxns![innerIndex]; + const innerTrace = this.trace.innerTrace![innerIndex]; innerGroupInfo.push(innerTxnInfo); innerTraces.push(innerTrace); } const expandedPath = this.txnPath.slice(); - expandedPath.push(this.innerTxnGroupCount); - const innerGroupFrame = new TransactionStackFrame( + expandedPath.push(spawnedInnerIndexes[0]); + let innerFailureInfo: TransactionFailureInfo | undefined = undefined; + if ( + this.failureInfo && + this.failureInfo.path.length > this.txnPath.length - 1 && + pathStartWith(this.failureInfo.path, this.txnPath.slice(1)) + ) { + innerFailureInfo = this.failureInfo; + } + const innerGroupFrame = new TransactionGroupStackFrame( this.engine, expandedPath, innerGroupInfo, innerTraces, + innerFailureInfo, ); stack.push(innerGroupFrame); this.handledInnerTxns = true; - this.innerTxnGroupCount++; return; } - if (this.index + 1 < this.programTrace.length) { - this.index++; + this.index++; + + if (this.index < this.programTrace.length) { this.state.pc = Number(this.programTrace[this.index].pc); this.handledInnerTxns = false; - return; + } else if ( + this.failureInfo && + pathsEqual(this.txnPath.slice(1), this.failureInfo.path) + ) { + // If there's an error, show it at the end of execution + this.blockingException = new ExceptionInfo(this.failureInfo.message); + return this.blockingException; } - stack.pop(); } private processUnit(unit: algosdk.modelsv2.SimulationOpcodeTraceUnit) { @@ -839,7 +954,10 @@ export class ProgramStackFrame extends TraceReplayStackFrame { } } - public backward(stack: TraceReplayStackFrame[]): void { + public backward(stack: TraceReplayStackFrame[]): ExceptionInfo | void { + if (this.blockingException) { + this.blockingException = undefined; + } if (this.handledInnerTxns) { // We can roll this back without any other effects this.handledInnerTxns = false; @@ -859,7 +977,6 @@ export class ProgramStackFrame extends TraceReplayStackFrame { private reset() { this.index = 0; this.handledInnerTxns = false; - this.innerTxnGroupCount = 0; this.state.pc = Number(this.programTrace[0].pc); this.state.stack = []; this.state.scratch.clear(); @@ -871,3 +988,30 @@ export class ProgramStackFrame extends TraceReplayStackFrame { } } } + +function pathsEqual(path1: number[], path2: number[]): boolean { + if (path1.length !== path2.length) { + return false; + } + for (let i = 0; i < path1.length; i++) { + if (path1[i] !== path2[i]) { + return false; + } + } + return true; +} + +/** + * Determines if the given path starts with the given prefix. + */ +function pathStartWith(path: number[], prefix: number[]): boolean { + if (path.length < prefix.length) { + return false; + } + for (let i = 0; i < prefix.length; i++) { + if (path[i] !== prefix[i]) { + return false; + } + } + return true; +} diff --git a/src/debugAdapter/utils.ts b/src/debugAdapter/utils.ts index d4e50f3..427b420 100644 --- a/src/debugAdapter/utils.ts +++ b/src/debugAdapter/utils.ts @@ -276,10 +276,23 @@ export class TEALDebuggingAssets { ); let simulateResponse: algosdk.modelsv2.SimulateResponse; try { + const jsonPased = parseJsonWithBigints( + rawSimulateTrace.toString('utf-8'), + ); + if (jsonPased.version !== 2) { + throw new Error( + `Unsupported simulate response version: ${jsonPased.version}`, + ); + } simulateResponse = - algosdk.modelsv2.SimulateResponse.from_obj_for_encoding( - parseJsonWithBigints(rawSimulateTrace.toString('utf-8')), + algosdk.modelsv2.SimulateResponse.from_obj_for_encoding(jsonPased); + if (!simulateResponse.execTraceConfig?.enable) { + throw new Error( + `Simulate response does not contain trace data. execTraceConfig=${JSON.stringify( + simulateResponse.execTraceConfig, + )}`, ); + } } catch (e) { const err = e as Error; throw new Error( diff --git a/tests/adapter.test.ts b/tests/adapter.test.ts index 6415e87..fd490c2 100644 --- a/tests/adapter.test.ts +++ b/tests/adapter.test.ts @@ -454,6 +454,7 @@ describe('Debug Adapter Tests', () => { { line: 93, column: 1 }, { line: 94, column: 1 }, { line: 95, column: 1 }, + { line: 95, column: 1 }, ].map((partial) => ({ ...partial, name: 'slot-machine.teal', @@ -537,6 +538,12 @@ describe('Debug Adapter Tests', () => { line: 7, column: 1, }, + { + program: lsigPath, + name: 'lsig.teal', + line: 7, + column: 1, + }, { name: 'transaction-group-0.json', line: 23, @@ -590,6 +597,12 @@ describe('Debug Adapter Tests', () => { line: 9, column: 1, }, + { + program: appPath, + name: 'app.teal', + line: 9, + column: 1, + }, { name: 'transaction-group-0.json', line: 33, @@ -636,6 +649,12 @@ describe('Debug Adapter Tests', () => { line: 7, column: 1, }, + { + program: lsigPath, + name: 'lsig.teal', + line: 7, + column: 1, + }, ]; for (let i = 0; i < expectedLocations.length; i++) { @@ -876,7 +895,7 @@ describe('Debug Adapter Tests', () => { }, { location: { - name: 'inner-transaction-group-0-0.json', + name: 'inner-transaction-group-0-1.json', line: 20, column: 0, }, @@ -1064,6 +1083,19 @@ describe('Debug Adapter Tests', () => { }, ); + // Reset breakpoints + await client.setBreakpointsRequest({ + source: { path: startLocation.program! }, + breakpoints: [], + }); + + // Since the first 2 locations are the same (due to before/after opcode execution), the + // breakpoint only hits the first. Must manually advance to the second. + assert.deepStrictEqual(expectedLocations[0], expectedLocations[1]); + await client.nextRequest({ threadId: 1 }); + const stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + for (let i = 0; i < expectedLocations.length; i++) { const expectedLocation = expectedLocations[i]; const stackTraceResponse = await client.stackTraceRequest({ @@ -1795,4 +1827,276 @@ describe('Debug Adapter Tests', () => { ]); }); }); + + describe('Errors Reporting', () => { + it('should correctly report an inner app error', async () => { + const simulateTraceFile = path.join( + DATA_ROOT, + 'errors/inner-app/app-reject-simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'errors/inner-app/sources.json', + ); + const { client } = fixture; + + const program = path.join(DATA_ROOT, 'errors/inner-app/inner.teal'); + + await Promise.all([ + client.configurationSequence(), + client.launch({ + simulateTraceFile, + programSourcesDescriptionFile, + stopOnEntry: true, + }), + client.assertStoppedLocation('entry', {}), + ]); + + await client.continueRequest({ threadId: 1 }); + await client.assertStoppedLocation('exception', { + path: program, + line: 7, + column: 1, + }); + const stoppedEvent = await client.waitForStop(); + assert.ok( + stoppedEvent.body.text?.includes( + 'logic eval error: logic eval error: assert failed pc=10', + ), + stoppedEvent.body.text, + ); + await assertVariables(client, { + pc: 10, + stack: [], + }); + + // Cannot walk forward over the error + await client.nextRequest({ threadId: 1 }); + await client.assertStoppedLocation('exception', { + path: program, + line: 7, + column: 1, + }); + await assertVariables(client, { + pc: 10, + stack: [], + }); + + // Can walk backwards + await client.stepBackRequest({ threadId: 1 }); + await client.assertStoppedLocation('step', { + path: program, + line: 7, + column: 1, + }); + await assertVariables(client, { + pc: 10, // We're at the same pc, but before the opcode ran, hence the stack value + stack: [0], + }); + + // And backwards again + await client.stepBackRequest({ threadId: 1 }); + await client.assertStoppedLocation('step', { + path: program, + line: 6, + column: 1, + }); + await assertVariables(client, { + pc: 9, + stack: [new Uint8Array(8)], + }); + + // Walking forward hits the error again + await client.continueRequest({ threadId: 1 }); + await client.assertStoppedLocation('exception', { + path: program, + line: 7, + column: 1, + }); + await assertVariables(client, { + pc: 10, + stack: [], + }); + }); + + it('should correctly report an inner transaction error', async () => { + const simulateTraceFile = path.join( + DATA_ROOT, + 'errors/inner-app/overspend-simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'errors/inner-app/sources.json', + ); + const { client } = fixture; + + await Promise.all([ + client.configurationSequence(), + client.launch({ + simulateTraceFile, + programSourcesDescriptionFile, + stopOnEntry: true, + }), + client.assertStoppedLocation('entry', {}), + ]); + + await client.continueRequest({ threadId: 1 }); + + let stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'exception'); + assert.ok( + stoppedEvent.body.text?.includes('logic eval error: overspend'), + stoppedEvent.body.text, + ); + let stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + let currentFrame = stackTraceResponse.body.stackFrames[0]; + assert.strictEqual( + currentFrame.source?.name, + 'inner-transaction-group-0-0.json', + ); + assert.strictEqual(currentFrame.line, 2); + + // Cannot walk forward over the error + await client.nextRequest({ threadId: 1 }); + stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'exception'); + stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + currentFrame = stackTraceResponse.body.stackFrames[0]; + assert.strictEqual( + currentFrame.source?.name, + 'inner-transaction-group-0-0.json', + ); + assert.strictEqual(currentFrame.line, 2); + + // Can walk backwards + await client.stepBackRequest({ threadId: 1 }); + stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'step'); + stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + currentFrame = stackTraceResponse.body.stackFrames[0]; + assert.strictEqual( + currentFrame.source?.name, + 'inner-transaction-group-0-0.json', + ); + assert.strictEqual(currentFrame.line, 2); + + // And backwards again + await client.stepBackRequest({ threadId: 1 }); + await client.assertStoppedLocation('step', { + path: path.join(DATA_ROOT, 'errors/inner-app/outer.teal'), + line: 12, + column: 1, + }); + + // Walking forward hits the error again + await client.continueRequest({ threadId: 1 }); + stoppedEvent = await client.waitForStop(); + assert.strictEqual(stoppedEvent.body.reason, 'exception'); + stackTraceResponse = await client.stackTraceRequest({ + threadId: 1, + }); + currentFrame = stackTraceResponse.body.stackFrames[0]; + assert.strictEqual( + currentFrame.source?.name, + 'inner-transaction-group-0-0.json', + ); + assert.strictEqual(currentFrame.line, 2); + }); + + it('should correctly report a LogicSig error', async () => { + const simulateTraceFile = path.join( + DATA_ROOT, + 'errors/logicsig/simulate-response.json', + ); + const programSourcesDescriptionFile = path.join( + DATA_ROOT, + 'errors/logicsig/sources.json', + ); + const { client } = fixture; + + const program = path.join(DATA_ROOT, 'errors/logicsig/lsig-err.teal'); + + await Promise.all([ + client.configurationSequence(), + client.launch({ + simulateTraceFile, + programSourcesDescriptionFile, + stopOnEntry: true, + }), + client.assertStoppedLocation('entry', {}), + ]); + + await client.continueRequest({ threadId: 1 }); + await client.assertStoppedLocation('exception', { + path: program, + line: 4, + column: 1, + }); + const stoppedEvent = await client.waitForStop(); + assert.ok( + stoppedEvent.body.text?.includes( + 'rejected by logic err=err opcode executed. Details: pc=4', + ), + stoppedEvent.body.text, + ); + await assertVariables(client, { + pc: 4, + stack: [0], + }); + + // Cannot walk forward over the error + await client.nextRequest({ threadId: 1 }); + await client.assertStoppedLocation('exception', { + path: program, + line: 4, + column: 1, + }); + await assertVariables(client, { + pc: 4, + stack: [0], + }); + + // Can walk backwards + await client.stepBackRequest({ threadId: 1 }); + await client.assertStoppedLocation('step', { + path: program, + line: 4, + column: 1, + }); + await assertVariables(client, { + pc: 4, // We're at the same pc, but before the opcode ran + stack: [0], + }); + + // And backwards again + await client.stepBackRequest({ threadId: 1 }); + await client.assertStoppedLocation('step', { + path: program, + line: 3, + column: 1, + }); + await assertVariables(client, { + pc: 3, + stack: [2000], + }); + + // Walking forward hits the error again + await client.continueRequest({ threadId: 1 }); + await client.assertStoppedLocation('exception', { + path: program, + line: 4, + column: 1, + }); + await assertVariables(client, { + pc: 4, + stack: [0], + }); + }); + }); });