diff --git a/.travis.yml b/.travis.yml index 6c58b08b..99aa8fed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,12 @@ dist: bionic language: python python: - "3.7" - script: - set -e + - if [ -n "$DOCKER_PASSWORD" ]; then echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin ; fi - python3 test_unit.py - make docker-test +env: + global: + - secure: CNZSqSglsDSMYKwxyTxoj6FrSCshvsl/9aXdnlFXl8Y/+zDIKCdV5Py0TgboPmhQtMBtYqkxLv7nItlB4jRjVHWXQ26J7tNhqZX4SEKvfmyKv94y10/HdV/rXJdYJLemobt2C2iheqBMMbg+py0Pyh5kyZfdORvyqukeh4XldhFbqmhSHomh2/juo7AiqRhk9L68TGWPkIvYjKvCx43SLu8c8eJXosdllFoOp69c1IjEEwlfBAUTXAyytZnogBkuzhCzjJgEbWT8x4umpud46vHvtUwmRExGa/wqhWlAo4KSeR59wX7T/BhG15w2DxqVWuzjn7s+26Il+crT3rKCPfrInDmu6A6dtFcYGl9+g8DYlXczs0hIPHRUvA8ieQpO2wMKMBUcR+4qsiXe8510ISy3WJRzJDmW2XwfrJ/tiLomeaA4MGWrOWsszxVQddTREKVP0PQeszLJQFA/RgyaYMzeZQPn9xXD+tJS7aAwlszdHgq+Hm1JIA8763b7jnuEPLimdZwrY4z3B9/y8or9I0tVhcRRbVipJKVQDCZrJpZE+Rh6oCnZ/GRGUrAiPJqKExO8HtH37025KTAxuCCYkHGHb1hEw3P0a63KUvaRzO4/5ElWp/sFJq1LxDehFyuONBdrOo3DY3scEs27GhTMtXUq1Z2kkTMi9AV5L/NYxK0= + - secure: GtSY5hon4bWzHTTVNB6CFPYgM+umWgiECPj0gnw/d6bmypwUxVnydBx4j6p+GYzwd6u7eByGXwPWQhrqsv5SLNS5683GS7qf89ab0bRhiQsW3J7s/QlmdRbMosw0mZ4E9JeQAC+IK/yAuqS+m0YcQuqks9ausJUgz0NjgjcJKvZTPjhi7up+kdJZ9nOkyIlJEJ6d4IJLYZT0RK3T5RapfTkw/NF15keYMtVV/Paf4vYyV0sqHwAt1DdT6bjpEj/2eN6WG+ISVKvzXWYhCcq86Yk89TqqY6hECTYRe+1pJqmTIJe2RNgNk9+W2NlOfpTswCG8bzLZbK08ph/QD61KzHIaya/IpCXnaEriW3wda7kkSoTPVOE4eiIQr7uG0zHY6nVO3EnxxM7YMRdaMUcCF//+SCjSIbbMKhUwKhz2gjbDQT0iPfgIPs5Hb75OhidcVWbqVmlWSiAQBEiCTYpVtx4HwNDZdozYCIS0Jw+I5aR7fVp4e1TrzbZ7OIIDgwrGmX1ZB4UXO1EMovWlkwRo4YylWafjrs/euqmQqMrG4XiTaWGvZUxuA/9qmDTNCGksn/idC8VVCN6/9MCrNSjjbbQBuIHaymHw/CUL8hbp80Ih6i+9Srq1QPaGBAPUEIDeHxljeyc1VJUcHB14883U2JRJPvAls1u+wRB9DsPpsoc= diff --git a/CHANGELOG.md b/CHANGELOG.md index 037b1b0c..021385f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 1.5.0 +### Added +- Support new features for indexer 2.3.2 +- Support for offline and nonparticipating key registration transactions. +- Add TEAL 3 support + +### BugFix +- Detects the sending of unsigned transactions +- Add asset_info() and application_info() methods to the v2 AlgodClient class. + ## 1.4.1 ## Bugfix - Dependency on missing constant removed diff --git a/Dockerfile b/Dockerfile index 3e35760a..f445d57a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.7 +FROM python:3.7.9 # Copy SDK code into the container RUN mkdir -p $HOME/py-algorand-sdk @@ -6,7 +6,7 @@ COPY . $HOME/py-algorand-sdk WORKDIR $HOME/py-algorand-sdk # SDK dependencies, and source version of behave with tag expression support -RUN pip3 install git+https://github.com/algorand/py-algorand-sdk/ -q \ +RUN pip install . -q \ && pip install git+https://github.com/behave/behave -q # Run integration tests diff --git a/Makefile b/Makefile index 257023c0..f8a681b5 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ unit: - behave --tags="@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.responses" test -f progress2 + behave --tags="@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.responses.231" test -f progress2 integration: - behave --tags="@algod or @assets or @auction or @kmd or @send or @template or @indexer or @indexer.applications or @rekey or @compile or @dryrun or @dryrun.testing or @applications or @applications.verified" test -f progress2 + behave --tags="@algod or @assets or @auction or @kmd or @send or @template or @indexer or @indexer.applications or @rekey or @compile or @dryrun or @dryrun.testing or @applications or @applications.verified or @indexer.231" test -f progress2 docker-test: ./run_integration.sh diff --git a/README.md b/README.md index f8eed285..1a5cdf06 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ else: ## Node setup -Follow the instructions in Algorand's [developer resources](https://developer.algorand.org/docs/introduction-installing-node) to install a node on your computer. +Follow the instructions in Algorand's [developer resources](https://developer.algorand.org/docs/run-a-node/setup/install/) to install a node on your computer. ## Running examples/example.py diff --git a/algosdk/constants.py b/algosdk/constants.py index 5acd2eb0..3867cb93 100644 --- a/algosdk/constants.py +++ b/algosdk/constants.py @@ -8,7 +8,7 @@ """str: header key for algod requests""" indexer_auth_header = "X-Indexer-API-Token" """str: header key for indexer requests""" -unversioned_paths = ["/health", "/versions", "/metrics"] +unversioned_paths = ["/health", "/versions", "/metrics", "/genesis"] """str[]: paths that don't use the version path prefix""" no_auth = [] """str[]: requests that don't require authentication""" @@ -55,6 +55,8 @@ """bytes: program (logic) data prefix when signing""" +hash_len = 32 +"""int: how long various hash-like fields should be""" check_sum_len_bytes = 4 """int: how long checksums should be""" key_len_bytes = 32 diff --git a/algosdk/data/langspec.json b/algosdk/data/langspec.json index 016969d9..e7a43c42 100644 --- a/algosdk/data/langspec.json +++ b/algosdk/data/langspec.json @@ -1 +1 @@ -{"EvalMaxVersion":2,"LogicSigVersion":2,"Ops":[{"Opcode":0,"Name":"err","Cost":1,"Size":1,"Doc":"Error. Panic immediately. This is primarily a fencepost against accidental zero bytes getting compiled into programs.","Groups":["Flow Control"]},{"Opcode":1,"Name":"sha256","Args":"B","Returns":"B","Cost":35,"Size":1,"Doc":"SHA256 hash of value X, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":2,"Name":"keccak256","Args":"B","Returns":"B","Cost":130,"Size":1,"Doc":"Keccak256 hash of value X, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":3,"Name":"sha512_256","Args":"B","Returns":"B","Cost":45,"Size":1,"Doc":"SHA512_256 hash of value X, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":4,"Name":"ed25519verify","Args":"BBB","Returns":"U","Cost":1900,"Size":1,"Doc":"for (data A, signature B, pubkey C) verify the signature of (\"ProgData\" || program_hash || data) against the pubkey =\u003e {0 or 1}","DocExtra":"The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack.","Groups":["Arithmetic"]},{"Opcode":8,"Name":"+","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A plus B. Panic on overflow.","DocExtra":"Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `plusw`.","Groups":["Arithmetic"]},{"Opcode":9,"Name":"-","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A minus B. Panic if B \u003e A.","Groups":["Arithmetic"]},{"Opcode":10,"Name":"/","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A divided by B. Panic if B == 0.","Groups":["Arithmetic"]},{"Opcode":11,"Name":"*","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A times B. Panic on overflow.","DocExtra":"Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`.","Groups":["Arithmetic"]},{"Opcode":12,"Name":"\u003c","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A less than B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":13,"Name":"\u003e","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A greater than B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":14,"Name":"\u003c=","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A less than or equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":15,"Name":"\u003e=","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A greater than or equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":16,"Name":"\u0026\u0026","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A is not zero and B is not zero =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":17,"Name":"||","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A is not zero or B is not zero =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":18,"Name":"==","Args":"..","Returns":"U","Cost":1,"Size":1,"Doc":"A is equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":19,"Name":"!=","Args":"..","Returns":"U","Cost":1,"Size":1,"Doc":"A is not equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":20,"Name":"!","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"X == 0 yields 1; else 0","Groups":["Arithmetic"]},{"Opcode":21,"Name":"len","Args":"B","Returns":"U","Cost":1,"Size":1,"Doc":"yields length of byte value X","Groups":["Arithmetic"]},{"Opcode":22,"Name":"itob","Args":"U","Returns":"B","Cost":1,"Size":1,"Doc":"converts uint64 X to big endian bytes","Groups":["Arithmetic"]},{"Opcode":23,"Name":"btoi","Args":"B","Returns":"U","Cost":1,"Size":1,"Doc":"converts bytes X as big endian to uint64","DocExtra":"`btoi` panics if the input is longer than 8 bytes.","Groups":["Arithmetic"]},{"Opcode":24,"Name":"%","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A modulo B. Panic if B == 0.","Groups":["Arithmetic"]},{"Opcode":25,"Name":"|","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-or B","Groups":["Arithmetic"]},{"Opcode":26,"Name":"\u0026","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-and B","Groups":["Arithmetic"]},{"Opcode":27,"Name":"^","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-xor B","Groups":["Arithmetic"]},{"Opcode":28,"Name":"~","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"bitwise invert value X","Groups":["Arithmetic"]},{"Opcode":29,"Name":"mulw","Args":"UU","Returns":"UU","Cost":1,"Size":1,"Doc":"A times B out to 128-bit long result as low (top) and high uint64 values on the stack","Groups":["Arithmetic"]},{"Opcode":30,"Name":"plusw","Args":"UU","Returns":"UU","Cost":1,"Size":1,"Doc":"A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack","Groups":["Arithmetic"]},{"Opcode":32,"Name":"intcblock","Cost":1,"Size":0,"Doc":"load block of uint64 constants","DocExtra":"`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script.","ImmediateNote":"{varuint length} [{varuint value}, ...]","Groups":["Loading Values"]},{"Opcode":33,"Name":"intc","Returns":"U","Cost":1,"Size":2,"Doc":"push value from uint64 constants to stack by index into constants","ImmediateNote":"{uint8 int constant index}","Groups":["Loading Values"]},{"Opcode":34,"Name":"intc_0","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 0 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":35,"Name":"intc_1","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 1 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":36,"Name":"intc_2","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 2 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":37,"Name":"intc_3","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 3 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":38,"Name":"bytecblock","Cost":1,"Size":0,"Doc":"load block of byte-array constants","DocExtra":"`bytecblock` loads the following program bytes into an array of byte string constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script.","ImmediateNote":"{varuint length} [({varuint value length} bytes), ...]","Groups":["Loading Values"]},{"Opcode":39,"Name":"bytec","Returns":"B","Cost":1,"Size":2,"Doc":"push bytes constant to stack by index into constants","ImmediateNote":"{uint8 byte constant index}","Groups":["Loading Values"]},{"Opcode":40,"Name":"bytec_0","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 0 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":41,"Name":"bytec_1","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 1 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":42,"Name":"bytec_2","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 2 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":43,"Name":"bytec_3","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 3 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":44,"Name":"arg","Returns":"B","Cost":1,"Size":2,"Doc":"push Args[N] value to stack by index","ImmediateNote":"{uint8 arg index N}","Groups":["Loading Values"]},{"Opcode":45,"Name":"arg_0","Returns":"B","Cost":1,"Size":1,"Doc":"push Args[0] to stack","Groups":["Loading Values"]},{"Opcode":46,"Name":"arg_1","Returns":"B","Cost":1,"Size":1,"Doc":"push Args[1] to stack","Groups":["Loading Values"]},{"Opcode":47,"Name":"arg_2","Returns":"B","Cost":1,"Size":1,"Doc":"push Args[2] to stack","Groups":["Loading Values"]},{"Opcode":48,"Name":"arg_3","Returns":"B","Cost":1,"Size":1,"Doc":"push Args[3] to stack","Groups":["Loading Values"]},{"Opcode":49,"Name":"txn","Returns":".","Cost":1,"Size":2,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBU","Doc":"push field from current transaction to stack","DocExtra":"FirstValidTime causes the program to fail. The field is reserved for future use.","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":50,"Name":"global","Returns":".","Cost":1,"Size":2,"ArgEnum":["MinTxnFee","MinBalance","MaxTxnLife","ZeroAddress","GroupSize","LogicSigVersion","Round","LatestTimestamp"],"ArgEnumTypes":"UUUBUUUU","Doc":"push value from globals to stack","ImmediateNote":"{uint8 global field index}","Groups":["Loading Values"]},{"Opcode":51,"Name":"gtxn","Returns":".","Cost":1,"Size":3,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBU","Doc":"push field to the stack from a transaction in the current transaction group","DocExtra":"for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`.","ImmediateNote":"{uint8 transaction group index}{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":52,"Name":"load","Returns":".","Cost":1,"Size":2,"Doc":"copy a value from scratch space to the stack","ImmediateNote":"{uint8 position in scratch space to load from}","Groups":["Loading Values"]},{"Opcode":53,"Name":"store","Args":".","Cost":1,"Size":2,"Doc":"pop a value from the stack and store to scratch space","ImmediateNote":"{uint8 position in scratch space to store to}","Groups":["Loading Values"]},{"Opcode":54,"Name":"txna","Returns":".","Cost":1,"Size":3,"ArgEnum":["ApplicationArgs","Accounts"],"ArgEnumTypes":"BB","Doc":"push value of an array field from current transaction to stack","ImmediateNote":"{uint8 transaction field index}{uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":55,"Name":"gtxna","Returns":".","Cost":1,"Size":4,"ArgEnum":["ApplicationArgs","Accounts"],"ArgEnumTypes":"BB","Doc":"push value of a field to the stack from a transaction in the current transaction group","ImmediateNote":"{uint8 transaction group index}{uint8 transaction field index}{uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":64,"Name":"bnz","Args":"U","Cost":1,"Size":3,"Doc":"branch if value X is not zero","DocExtra":"The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping.\n\nAt LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction, removing the need for a last instruction or no-op as a branch target at the end. Branching beyond that may still fail the program.","ImmediateNote":"{0..0x7fff forward branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":65,"Name":"bz","Args":"U","Cost":1,"Size":3,"Doc":"branch if value X is zero","DocExtra":"See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`.","ImmediateNote":"{0..0x7fff forward branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":66,"Name":"b","Cost":1,"Size":3,"Doc":"branch unconditionally to offset","DocExtra":"See `bnz` for details on how branches work. `b` always jumps to the offset.","ImmediateNote":"{0..0x7fff forward branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":67,"Name":"return","Args":"U","Cost":1,"Size":1,"Doc":"use last value on stack as success value; end","Groups":["Flow Control"]},{"Opcode":72,"Name":"pop","Args":".","Cost":1,"Size":1,"Doc":"discard value X from stack","Groups":["Flow Control"]},{"Opcode":73,"Name":"dup","Args":".","Returns":"..","Cost":1,"Size":1,"Doc":"duplicate last value on stack","Groups":["Flow Control"]},{"Opcode":74,"Name":"dup2","Args":"..","Returns":"....","Cost":1,"Size":1,"Doc":"duplicate two last values on stack: A, B -\u003e A, B, A, B","Groups":["Flow Control"]},{"Opcode":80,"Name":"concat","Args":"BB","Returns":"B","Cost":1,"Size":1,"Doc":"pop two byte strings A and B and join them, push the result","DocExtra":"`concat` panics if the result would be greater than 4096 bytes.","Groups":["Arithmetic"]},{"Opcode":81,"Name":"substring","Args":"B","Returns":"B","Cost":1,"Size":3,"Doc":"pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result","ImmediateNote":"{uint8 start position}{uint8 end position}","Groups":["Arithmetic"]},{"Opcode":82,"Name":"substring3","Args":"BUU","Returns":"B","Cost":1,"Size":1,"Doc":"pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result","Groups":["Arithmetic"]},{"Opcode":96,"Name":"balance","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction","Groups":["State Access"]},{"Opcode":97,"Name":"app_opted_in","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"check if account specified by Txn.Accounts[A] opted in for the application B =\u003e {0 or 1}","DocExtra":"params: account index, application id (top of the stack on opcode entry). Return: 1 if opted in and 0 otherwise.","Groups":["State Access"]},{"Opcode":98,"Name":"app_local_get","Args":"UB","Returns":".","Cost":1,"Size":1,"Doc":"read from account specified by Txn.Accounts[A] from local state of the current application key B =\u003e value","DocExtra":"params: account index, state key. Return: value. The value is zero if the key does not exist.","Groups":["State Access"]},{"Opcode":99,"Name":"app_local_get_ex","Args":"UUB","Returns":"U.","Cost":1,"Size":1,"Doc":"read from account specified by Txn.Accounts[A] from local state of the application B key C =\u003e {0 or 1 (top), value}","DocExtra":"params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value.","Groups":["State Access"]},{"Opcode":100,"Name":"app_global_get","Args":"B","Returns":".","Cost":1,"Size":1,"Doc":"read key A from global state of a current application =\u003e value","DocExtra":"params: state key. Return: value. The value is zero if the key does not exist.","Groups":["State Access"]},{"Opcode":101,"Name":"app_global_get_ex","Args":"UB","Returns":"U.","Cost":1,"Size":1,"Doc":"read from application A global state key B =\u003e {0 or 1 (top), value}","DocExtra":"params: application id, state key. Return: value.","Groups":["State Access"]},{"Opcode":102,"Name":"app_local_put","Args":"UB.","Cost":1,"Size":1,"Doc":"write to account specified by Txn.Accounts[A] to local state of a current application key B with value C","DocExtra":"params: account index, state key, value.","Groups":["State Access"]},{"Opcode":103,"Name":"app_global_put","Args":"B.","Cost":1,"Size":1,"Doc":"write key A and value B to global state of the current application","Groups":["State Access"]},{"Opcode":104,"Name":"app_local_del","Args":"UB","Cost":1,"Size":1,"Doc":"delete from account specified by Txn.Accounts[A] local state key B of the current application","DocExtra":"params: account index, state key.","Groups":["State Access"]},{"Opcode":105,"Name":"app_global_del","Args":"B","Cost":1,"Size":1,"Doc":"delete key A from a global state of the current application","DocExtra":"params: state key.","Groups":["State Access"]},{"Opcode":112,"Name":"asset_holding_get","Args":"UU","Returns":"U.","Cost":1,"Size":2,"ArgEnum":["AssetBalance","AssetFrozen"],"ArgEnumTypes":"UU","Doc":"read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) =\u003e {0 or 1 (top), value}","DocExtra":"params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value.","ImmediateNote":"{uint8 asset holding field index}","Groups":["State Access"]},{"Opcode":113,"Name":"asset_params_get","Args":"UU","Returns":"U.","Cost":1,"Size":2,"ArgEnum":["AssetTotal","AssetDecimals","AssetDefaultFrozen","AssetUnitName","AssetName","AssetURL","AssetMetadataHash","AssetManager","AssetReserve","AssetFreeze","AssetClawback"],"ArgEnumTypes":"UUUBBBBBBBB","Doc":"read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) =\u003e {0 or 1 (top), value}","DocExtra":"params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value.","ImmediateNote":"{uint8 asset params field index}","Groups":["State Access"]}]} +{"EvalMaxVersion":3,"LogicSigVersion":3,"Ops":[{"Opcode":0,"Name":"err","Cost":1,"Size":1,"Doc":"Error. Panic immediately. This is primarily a fencepost against accidental zero bytes getting compiled into programs.","Groups":["Flow Control"]},{"Opcode":1,"Name":"sha256","Args":"B","Returns":"B","Cost":35,"Size":1,"Doc":"SHA256 hash of value X, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":2,"Name":"keccak256","Args":"B","Returns":"B","Cost":130,"Size":1,"Doc":"Keccak256 hash of value X, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":3,"Name":"sha512_256","Args":"B","Returns":"B","Cost":45,"Size":1,"Doc":"SHA512_256 hash of value X, yields [32]byte","Groups":["Arithmetic"]},{"Opcode":4,"Name":"ed25519verify","Args":"BBB","Returns":"U","Cost":1900,"Size":1,"Doc":"for (data A, signature B, pubkey C) verify the signature of (\"ProgData\" || program_hash || data) against the pubkey =\u003e {0 or 1}","DocExtra":"The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack.","Groups":["Arithmetic"]},{"Opcode":8,"Name":"+","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A plus B. Panic on overflow.","DocExtra":"Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `addw`.","Groups":["Arithmetic"]},{"Opcode":9,"Name":"-","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A minus B. Panic if B \u003e A.","Groups":["Arithmetic"]},{"Opcode":10,"Name":"/","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A divided by B. Panic if B == 0.","Groups":["Arithmetic"]},{"Opcode":11,"Name":"*","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A times B. Panic on overflow.","DocExtra":"Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`.","Groups":["Arithmetic"]},{"Opcode":12,"Name":"\u003c","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A less than B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":13,"Name":"\u003e","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A greater than B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":14,"Name":"\u003c=","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A less than or equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":15,"Name":"\u003e=","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A greater than or equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":16,"Name":"\u0026\u0026","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A is not zero and B is not zero =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":17,"Name":"||","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A is not zero or B is not zero =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":18,"Name":"==","Args":"..","Returns":"U","Cost":1,"Size":1,"Doc":"A is equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":19,"Name":"!=","Args":"..","Returns":"U","Cost":1,"Size":1,"Doc":"A is not equal to B =\u003e {0 or 1}","Groups":["Arithmetic"]},{"Opcode":20,"Name":"!","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"X == 0 yields 1; else 0","Groups":["Arithmetic"]},{"Opcode":21,"Name":"len","Args":"B","Returns":"U","Cost":1,"Size":1,"Doc":"yields length of byte value X","Groups":["Arithmetic"]},{"Opcode":22,"Name":"itob","Args":"U","Returns":"B","Cost":1,"Size":1,"Doc":"converts uint64 X to big endian bytes","Groups":["Arithmetic"]},{"Opcode":23,"Name":"btoi","Args":"B","Returns":"U","Cost":1,"Size":1,"Doc":"converts bytes X as big endian to uint64","DocExtra":"`btoi` panics if the input is longer than 8 bytes.","Groups":["Arithmetic"]},{"Opcode":24,"Name":"%","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A modulo B. Panic if B == 0.","Groups":["Arithmetic"]},{"Opcode":25,"Name":"|","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-or B","Groups":["Arithmetic"]},{"Opcode":26,"Name":"\u0026","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-and B","Groups":["Arithmetic"]},{"Opcode":27,"Name":"^","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"A bitwise-xor B","Groups":["Arithmetic"]},{"Opcode":28,"Name":"~","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"bitwise invert value X","Groups":["Arithmetic"]},{"Opcode":29,"Name":"mulw","Args":"UU","Returns":"UU","Cost":1,"Size":1,"Doc":"A times B out to 128-bit long result as low (top) and high uint64 values on the stack","Groups":["Arithmetic"]},{"Opcode":30,"Name":"addw","Args":"UU","Returns":"UU","Cost":1,"Size":1,"Doc":"A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack","Groups":["Arithmetic"]},{"Opcode":32,"Name":"intcblock","Cost":1,"Size":0,"Doc":"prepare block of uint64 constants for use by intc","DocExtra":"`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script.","ImmediateNote":"{varuint length} [{varuint value}, ...]","Groups":["Loading Values"]},{"Opcode":33,"Name":"intc","Returns":"U","Cost":1,"Size":2,"Doc":"push Ith constant from intcblock to stack","ImmediateNote":"{uint8 int constant index}","Groups":["Loading Values"]},{"Opcode":34,"Name":"intc_0","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 0 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":35,"Name":"intc_1","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 1 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":36,"Name":"intc_2","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 2 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":37,"Name":"intc_3","Returns":"U","Cost":1,"Size":1,"Doc":"push constant 3 from intcblock to stack","Groups":["Loading Values"]},{"Opcode":38,"Name":"bytecblock","Cost":1,"Size":0,"Doc":"prepare block of byte-array constants for use by bytec","DocExtra":"`bytecblock` loads the following program bytes into an array of byte-array constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script.","ImmediateNote":"{varuint length} [({varuint value length} bytes), ...]","Groups":["Loading Values"]},{"Opcode":39,"Name":"bytec","Returns":"B","Cost":1,"Size":2,"Doc":"push Ith constant from bytecblock to stack","ImmediateNote":"{uint8 byte constant index}","Groups":["Loading Values"]},{"Opcode":40,"Name":"bytec_0","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 0 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":41,"Name":"bytec_1","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 1 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":42,"Name":"bytec_2","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 2 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":43,"Name":"bytec_3","Returns":"B","Cost":1,"Size":1,"Doc":"push constant 3 from bytecblock to stack","Groups":["Loading Values"]},{"Opcode":44,"Name":"arg","Returns":"B","Cost":1,"Size":2,"Doc":"push Nth LogicSig argument to stack","ImmediateNote":"{uint8 arg index N}","Groups":["Loading Values"]},{"Opcode":45,"Name":"arg_0","Returns":"B","Cost":1,"Size":1,"Doc":"push LogicSig argument 0 to stack","Groups":["Loading Values"]},{"Opcode":46,"Name":"arg_1","Returns":"B","Cost":1,"Size":1,"Doc":"push LogicSig argument 1 to stack","Groups":["Loading Values"]},{"Opcode":47,"Name":"arg_2","Returns":"B","Cost":1,"Size":1,"Doc":"push LogicSig argument 2 to stack","Groups":["Loading Values"]},{"Opcode":48,"Name":"arg_3","Returns":"B","Cost":1,"Size":1,"Doc":"push LogicSig argument 3 to stack","Groups":["Loading Values"]},{"Opcode":49,"Name":"txn","Returns":".","Cost":1,"Size":2,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUU","Doc":"push field F of current transaction to stack","DocExtra":"FirstValidTime causes the program to fail. The field is reserved for future use.","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":50,"Name":"global","Returns":".","Cost":1,"Size":2,"ArgEnum":["MinTxnFee","MinBalance","MaxTxnLife","ZeroAddress","GroupSize","LogicSigVersion","Round","LatestTimestamp","CurrentApplicationID","CreatorAddress"],"ArgEnumTypes":"UUUBUUUUUB","Doc":"push value from globals to stack","ImmediateNote":"{uint8 global field index}","Groups":["Loading Values"]},{"Opcode":51,"Name":"gtxn","Returns":".","Cost":1,"Size":3,"ArgEnum":["Sender","Fee","FirstValid","FirstValidTime","LastValid","Note","Lease","Receiver","Amount","CloseRemainderTo","VotePK","SelectionPK","VoteFirst","VoteLast","VoteKeyDilution","Type","TypeEnum","XferAsset","AssetAmount","AssetSender","AssetReceiver","AssetCloseTo","GroupIndex","TxID","ApplicationID","OnCompletion","ApplicationArgs","NumAppArgs","Accounts","NumAccounts","ApprovalProgram","ClearStateProgram","RekeyTo","ConfigAsset","ConfigAssetTotal","ConfigAssetDecimals","ConfigAssetDefaultFrozen","ConfigAssetUnitName","ConfigAssetName","ConfigAssetURL","ConfigAssetMetadataHash","ConfigAssetManager","ConfigAssetReserve","ConfigAssetFreeze","ConfigAssetClawback","FreezeAsset","FreezeAssetAccount","FreezeAssetFrozen","Assets","NumAssets","Applications","NumApplications","GlobalNumUint","GlobalNumByteSlice","LocalNumUint","LocalNumByteSlice"],"ArgEnumTypes":"BUUUUBBBUBBBUUUBUUUBBBUBUUBUBUBBBUUUUBBBBBBBBUBUUUUUUUUU","Doc":"push field F of the Tth transaction in the current group","DocExtra":"for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`.","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":52,"Name":"load","Returns":".","Cost":1,"Size":2,"Doc":"copy a value from scratch space to the stack","ImmediateNote":"{uint8 position in scratch space to load from}","Groups":["Loading Values"]},{"Opcode":53,"Name":"store","Args":".","Cost":1,"Size":2,"Doc":"pop a value from the stack and store to scratch space","ImmediateNote":"{uint8 position in scratch space to store to}","Groups":["Loading Values"]},{"Opcode":54,"Name":"txna","Returns":".","Cost":1,"Size":3,"ArgEnum":["ApplicationArgs","Accounts"],"ArgEnumTypes":"BBUU","Doc":"push Ith value of the array field F of the current transaction","ImmediateNote":"{uint8 transaction field index} {uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":55,"Name":"gtxna","Returns":".","Cost":1,"Size":4,"ArgEnum":["ApplicationArgs","Accounts"],"ArgEnumTypes":"BBUU","Doc":"push Ith value of the array field F from the Tth transaction in the current group","ImmediateNote":"{uint8 transaction group index} {uint8 transaction field index} {uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":56,"Name":"gtxns","Args":"U","Returns":".","Cost":1,"Size":2,"Doc":"push field F of the Ath transaction in the current group","DocExtra":"for notes on transaction fields available, see `txn`. If top of stack is _i_, `gtxns field` is equivalent to `gtxn _i_ field`. gtxns exists so that _i_ can be calculated, often based on the index of the current transaction.","ImmediateNote":"{uint8 transaction field index}","Groups":["Loading Values"]},{"Opcode":57,"Name":"gtxnsa","Args":"U","Returns":".","Cost":1,"Size":3,"Doc":"push Ith value of the array field F from the Ath transaction in the current group","ImmediateNote":"{uint8 transaction field index} {uint8 transaction field array index}","Groups":["Loading Values"]},{"Opcode":64,"Name":"bnz","Args":"U","Cost":1,"Size":3,"Doc":"branch to TARGET if value X is not zero","DocExtra":"The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping.\n\nAt LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction: bnz to byte N (with 0-indexing) was illegal for a TEAL program with N bytes before LogicSigVersion 2, and is legal after it. This change eliminates the need for a last instruction of no-op as a branch target at the end. (Branching beyond the end--in other words, to a byte larger than N--is still illegal and will cause the program to fail.)","ImmediateNote":"{0..0x7fff forward branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":65,"Name":"bz","Args":"U","Cost":1,"Size":3,"Doc":"branch to TARGET if value X is zero","DocExtra":"See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`.","ImmediateNote":"{0..0x7fff forward branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":66,"Name":"b","Cost":1,"Size":3,"Doc":"branch unconditionally to TARGET","DocExtra":"See `bnz` for details on how branches work. `b` always jumps to the offset.","ImmediateNote":"{0..0x7fff forward branch offset, big endian}","Groups":["Flow Control"]},{"Opcode":67,"Name":"return","Args":"U","Cost":1,"Size":1,"Doc":"use last value on stack as success value; end","Groups":["Flow Control"]},{"Opcode":68,"Name":"assert","Args":"U","Cost":1,"Size":1,"Doc":"immediately fail unless value X is a non-zero number","Groups":["Flow Control"]},{"Opcode":72,"Name":"pop","Args":".","Cost":1,"Size":1,"Doc":"discard value X from stack","Groups":["Flow Control"]},{"Opcode":73,"Name":"dup","Args":".","Returns":"..","Cost":1,"Size":1,"Doc":"duplicate last value on stack","Groups":["Flow Control"]},{"Opcode":74,"Name":"dup2","Args":"..","Returns":"....","Cost":1,"Size":1,"Doc":"duplicate two last values on stack: A, B -\u003e A, B, A, B","Groups":["Flow Control"]},{"Opcode":75,"Name":"dig","Args":".","Returns":"..","Cost":1,"Size":2,"Doc":"push the Nth value from the top of the stack. dig 0 is equivalent to dup","ImmediateNote":"{uint8 depth}","Groups":["Flow Control"]},{"Opcode":76,"Name":"swap","Args":"..","Returns":"..","Cost":1,"Size":1,"Doc":"swaps two last values on stack: A, B -\u003e B, A","Groups":["Flow Control"]},{"Opcode":77,"Name":"select","Args":"..U","Returns":".","Cost":1,"Size":1,"Doc":"selects one of two values based on top-of-stack: A, B, C -\u003e (if C != 0 then B else A)","Groups":["Flow Control"]},{"Opcode":80,"Name":"concat","Args":"BB","Returns":"B","Cost":1,"Size":1,"Doc":"pop two byte-arrays A and B and join them, push the result","DocExtra":"`concat` panics if the result would be greater than 4096 bytes.","Groups":["Arithmetic"]},{"Opcode":81,"Name":"substring","Args":"B","Returns":"B","Cost":1,"Size":3,"Doc":"pop a byte-array A. For immediate values in 0..255 S and E: extract a range of bytes from A starting at S up to but not including E, push the substring result. If E \u003c S, or either is larger than the array length, the program fails","ImmediateNote":"{uint8 start position} {uint8 end position}","Groups":["Arithmetic"]},{"Opcode":82,"Name":"substring3","Args":"BUU","Returns":"B","Cost":1,"Size":1,"Doc":"pop a byte-array A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C \u003c B, or either is larger than the array length, the program fails","Groups":["Arithmetic"]},{"Opcode":83,"Name":"getbit","Args":".U","Returns":"U","Cost":1,"Size":1,"Doc":"pop a target A (integer or byte-array), and index B. Push the Bth bit of A.","DocExtra":"see explanation of bit ordering in setbit","Groups":["Arithmetic"]},{"Opcode":84,"Name":"setbit","Args":".UU","Returns":"U","Cost":1,"Size":1,"Doc":"pop a target A, index B, and bit C. Set the Bth bit of A to C, and push the result","DocExtra":"bit indexing begins with low-order bits in integers. Setting bit 4 to 1 on the integer 0 yields 16 (`int 0x0010`, or 2^4). Indexing begins in the first bytes of a byte-string (as seen in getbyte and substring). Setting bits 0 through 11 to 1 in a 4 byte-array of 0s yields `byte 0xfff00000`","Groups":["Arithmetic"]},{"Opcode":85,"Name":"getbyte","Args":"BU","Returns":"U","Cost":1,"Size":1,"Doc":"pop a byte-array A and integer B. Extract the Bth byte of A and push it as an integer","Groups":["Arithmetic"]},{"Opcode":86,"Name":"setbyte","Args":"BUU","Returns":"B","Cost":1,"Size":1,"Doc":"pop a byte-array A, integer B, and small integer C (between 0..255). Set the Bth byte of A to C, and push the result","Groups":["Arithmetic"]},{"Opcode":96,"Name":"balance","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. The balance is observed after the effects of previous transactions in the group, and after the fee for the current transaction is deducted.","Groups":["State Access"]},{"Opcode":97,"Name":"app_opted_in","Args":"UU","Returns":"U","Cost":1,"Size":1,"Doc":"check if account specified by Txn.Accounts[A] opted in for the application B =\u003e {0 or 1}","DocExtra":"params: account index, application id (top of the stack on opcode entry). Return: 1 if opted in and 0 otherwise.","Groups":["State Access"]},{"Opcode":98,"Name":"app_local_get","Args":"UB","Returns":".","Cost":1,"Size":1,"Doc":"read from account specified by Txn.Accounts[A] from local state of the current application key B =\u003e value","DocExtra":"params: account index, state key. Return: value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":99,"Name":"app_local_get_ex","Args":"UUB","Returns":".U","Cost":1,"Size":1,"Doc":"read from account specified by Txn.Accounts[A] from local state of the application B key C =\u003e [*... stack*, value, 0 or 1]","DocExtra":"params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":100,"Name":"app_global_get","Args":"B","Returns":".","Cost":1,"Size":1,"Doc":"read key A from global state of a current application =\u003e value","DocExtra":"params: state key. Return: value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":101,"Name":"app_global_get_ex","Args":"UB","Returns":".U","Cost":1,"Size":1,"Doc":"read from application Txn.ForeignApps[A] global state key B =\u003e [*... stack*, value, 0 or 1]. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app","DocExtra":"params: application index, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. The value is zero (of type uint64) if the key does not exist.","Groups":["State Access"]},{"Opcode":102,"Name":"app_local_put","Args":"UB.","Cost":1,"Size":1,"Doc":"write to account specified by Txn.Accounts[A] to local state of a current application key B with value C","DocExtra":"params: account index, state key, value.","Groups":["State Access"]},{"Opcode":103,"Name":"app_global_put","Args":"B.","Cost":1,"Size":1,"Doc":"write key A and value B to global state of the current application","Groups":["State Access"]},{"Opcode":104,"Name":"app_local_del","Args":"UB","Cost":1,"Size":1,"Doc":"delete from account specified by Txn.Accounts[A] local state key B of the current application","DocExtra":"params: account index, state key.\n\nDeleting a key which is already absent has no effect on the application local state. (In particular, it does _not_ cause the program to fail.)","Groups":["State Access"]},{"Opcode":105,"Name":"app_global_del","Args":"B","Cost":1,"Size":1,"Doc":"delete key A from a global state of the current application","DocExtra":"params: state key.\n\nDeleting a key which is already absent has no effect on the application global state. (In particular, it does _not_ cause the program to fail.)","Groups":["State Access"]},{"Opcode":112,"Name":"asset_holding_get","Args":"UU","Returns":".U","Cost":1,"Size":2,"ArgEnum":["AssetBalance","AssetFrozen"],"ArgEnumTypes":"UU","Doc":"read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) =\u003e {0 or 1 (top), value}","DocExtra":"params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value.","ImmediateNote":"{uint8 asset holding field index}","Groups":["State Access"]},{"Opcode":113,"Name":"asset_params_get","Args":"U","Returns":".U","Cost":1,"Size":2,"ArgEnum":["AssetTotal","AssetDecimals","AssetDefaultFrozen","AssetUnitName","AssetName","AssetURL","AssetMetadataHash","AssetManager","AssetReserve","AssetFreeze","AssetClawback"],"ArgEnumTypes":"UUUBBBBBBBB","Doc":"read from asset Txn.ForeignAssets[A] params field X (imm arg) =\u003e {0 or 1 (top), value}","DocExtra":"params: txn.ForeignAssets offset. Return: did_exist flag (1 if exist and 0 otherwise), value.","ImmediateNote":"{uint8 asset params field index}","Groups":["State Access"]},{"Opcode":120,"Name":"min_balance","Args":"U","Returns":"U","Cost":1,"Size":1,"Doc":"get minimum required balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender. Required balance is affected by [ASA](https://developer.algorand.org/docs/features/asa/#assets-overview) and [App](https://developer.algorand.org/docs/features/asc1/stateful/#minimum-balance-requirement-for-a-smart-contract) usage. When creating or opting into an app, the minimum balance grows before the app code runs, therefore the increase is visible there. When deleting or closing out, the minimum balance decreases after the app executes.","Groups":["State Access"]},{"Opcode":128,"Name":"pushbytes","Returns":"B","Cost":1,"Size":0,"Doc":"push the following program bytes to the stack","ImmediateNote":"{varuint length} {bytes}","Groups":["Loading Values"]},{"Opcode":129,"Name":"pushint","Returns":"U","Cost":1,"Size":0,"Doc":"push immediate UINT to the stack as an integer","ImmediateNote":"{varuint int}","Groups":["Loading Values"]}]} diff --git a/algosdk/error.py b/algosdk/error.py index cbb72aea..2d855c0e 100644 --- a/algosdk/error.py +++ b/algosdk/error.py @@ -45,6 +45,13 @@ def __init__(self): Exception.__init__(self, "mnemonic length must be 25") +class WrongHashLengthError(Exception): + """General error that is normally changed to be more specific""" + + def __init(self): + Exception.__init__(self, "length must be 32 bytes") + + class WrongKeyBytesLengthError(Exception): def __init__(self): Exception.__init__(self, "key length in bytes must be 32") diff --git a/algosdk/future/transaction.py b/algosdk/future/transaction.py index 663daa2b..577d7a6a 100644 --- a/algosdk/future/transaction.py +++ b/algosdk/future/transaction.py @@ -59,22 +59,45 @@ def __init__(self, sender, sp, note, lease, txn_type, rekey_to): self.fee = sp.fee self.first_valid_round = sp.first self.last_valid_round = sp.last - self.note = note - if self.note is not None: - if not isinstance(self.note, bytes): - raise error.WrongNoteType - if len(self.note) > constants.note_max_length: - raise error.WrongNoteLength + self.note = self.as_note(note) self.genesis_id = sp.gen self.genesis_hash = sp.gh self.group = None - self.lease = lease - if self.lease is not None: - if len(self.lease) != constants.lease_length: - raise error.WrongLeaseLengthError + self.lease = self.as_lease(lease) self.type = txn_type self.rekey_to = rekey_to + @staticmethod + def as_hash(hash): + """Confirm that a value is 32 bytes. If all zeros, or a falsy value, return None""" + if not hash: + return None + assert isinstance(hash, (bytes, bytearray)), f"{hash} is not bytes" + if len(hash) != constants.hash_len: + raise error.WrongHashLengthError + if not any(hash): + return None + return hash + + @staticmethod + def as_note(note): + if not note: + return None + if not isinstance(note, (bytes, bytearray, str)): + raise error.WrongNoteType + if isinstance(note, str): + note = note.encode() + if len(note) > constants.note_max_length: + raise error.WrongNoteLength + return note + + @classmethod + def as_lease(cls, lease): + try: + return cls.as_hash(lease) + except error.WrongHashLengthError: + raise error.WrongLeaseLengthError + def get_txid(self): """ Get the transaction's ID. @@ -225,6 +248,31 @@ def __eq__(self, other): self.type == other.type and self.rekey_to == other.rekey_to) + @staticmethod + def required(arg): + if not arg: + raise ValueError(f"{arg} supplied as a required argument") + return arg + + @staticmethod + def creatable_index(index, required=False): + """Coerce an index for apps or assets to an integer. + + By using this in all constructors, we allow callers to use + strings as indexes, check our convenience Txn types to ensure + index is set, and ensure that 0 is always used internally for + an unset id, not None, so __eq__ works properly. + """ + i = int(index or 0) + if i == 0 and required: + raise IndexError("Required an index") + if i < 0: + raise IndexError(i) + return i + + def __str__(self): + return str(self.__dict__) + class PaymentTxn(Transaction): """ @@ -335,6 +383,7 @@ class KeyregTxn(Transaction): with the same sender and lease can be confirmed in this transaction's valid rounds rekey_to (str, optional): additionally rekey the sender to this address + nonpart (bool, optional): mark the account non-participating if true Attributes: sender (str) @@ -353,11 +402,12 @@ class KeyregTxn(Transaction): type (str) lease (byte[32]) rekey_to (str) + nonpart (bool) """ def __init__(self, sender, sp, votekey, selkey, votefst, votelst, votekd, note=None, - lease=None, rekey_to=None): + lease=None, rekey_to=None, nonpart=None): Transaction.__init__(self, sender, sp, note, lease, constants.keyreg_txn, rekey_to) self.votepk = votekey @@ -365,6 +415,7 @@ def __init__(self, sender, sp, votekey, selkey, votefst, self.votefst = votefst self.votelst = votelst self.votekd = votekd + self.nonpart = nonpart if sp.flat_fee: self.fee = max(constants.min_txn_fee, self.fee) else: @@ -373,11 +424,12 @@ def __init__(self, sender, sp, votekey, selkey, votefst, def dictify(self): d = { - "selkey": base64.b64decode(self.selkey), + "selkey": base64.b64decode(self.selkey) if self.selkey is not None else None, "votefst": self.votefst, "votekd": self.votekd, - "votekey": base64.b64decode(self.votepk), - "votelst": self.votelst + "votekey": base64.b64decode(self.votepk) if self.votepk is not None else None, + "votelst": self.votelst, + "nonpart": self.nonpart } d.update(super(KeyregTxn, self).dictify()) od = OrderedDict(sorted(d.items())) @@ -386,12 +438,31 @@ def dictify(self): @staticmethod def _undictify(d): + votekey = None + selkey = None + votefst = None + votelst = None + votekd = None + nonpart = None + + if "votekey" in d: + votekey = base64.b64encode(d["votekey"]).decode() + if "selkey" in d: + selkey = base64.b64encode(d["selkey"]).decode() + if "votefst" in d: + votefst = d["votefst"] + if "votelst" in d: + votelst = d["votelst"] + if "nonpart" in d: + nonpart = d["nonpart"] + args = { - "votekey": base64.b64encode(d["votekey"]).decode(), - "selkey": base64.b64encode(d["selkey"]).decode(), - "votefst": d["votefst"], - "votelst": d["votelst"], - "votekd": d["votekd"] + "votekey": votekey, + "selkey": selkey, + "votefst": votefst, + "votelst": votelst, + "votekd": votekd, + "nonpart": nonpart } return args @@ -497,9 +568,9 @@ def __init__( if strict_empty_address_check: if not (manager and reserve and freeze and clawback): raise error.EmptyAddressError - self.index = index - self.total = total - self.default_frozen = default_frozen + self.index = self.creatable_index(index) + self.total = int(total) if total else None + self.default_frozen = bool(default_frozen) self.unit_name = unit_name self.asset_name = asset_name self.manager = manager @@ -507,13 +578,10 @@ def __init__( self.freeze = freeze self.clawback = clawback self.url = url - self.metadata_hash = metadata_hash - self.decimals = decimals - if decimals < 0 or decimals > constants.max_asset_decimals: + self.metadata_hash = self.as_metadata(metadata_hash) + self.decimals = int(decimals) + if self.decimals < 0 or self.decimals > constants.max_asset_decimals: raise error.OutOfRangeDecimalsError - if metadata_hash is not None: - if len(metadata_hash) != constants.metadata_length: - raise error.WrongMetadataLengthError if sp.flat_fee: self.fee = max(constants.min_txn_fee, self.fee) else: @@ -637,8 +705,125 @@ def __eq__(self, other): self.metadata_hash == other.metadata_hash and self.decimals == other.decimals) + @classmethod + def as_metadata(cls, md): + try: + return cls.as_hash(md) + except error.WrongHashLengthError: + raise error.WrongMetadataLengthError + + + +class AssetCreateTxn(AssetConfigTxn): + """Represents a transaction for asset creation. + + Keyword arguments are required, starting with the special + addresses, to prevent errors, as type checks can't prevent simple + confusion of similar typed arguments. Since the special addresses + are required, strict_empty_address_check is turned off. + + Args: + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + total (int): total number of base units of this asset created + decimals (int, optional): number of digits to use for display after + decimal. If set to 0, the asset is not divisible. If set to 1, the + base unit of the asset is in tenths. Must be between 0 and 19, + inclusive. Defaults to 0. + default_frozen (bool): whether slots for this asset in user + accounts are frozen by default + manager (str): address allowed to change nonzero addresses + for this asset + reserve (str): account whose holdings of this asset should + be reported as "not minted" + freeze (str): account allowed to change frozen state of + holdings of this asset + clawback (str): account allowed take units of this asset + from any account + unit_name (str): hint for the name of a unit of this asset + asset_name (str): hint for the name of the asset + url (str): a URL where more information about the asset + can be retrieved + metadata_hash (byte[32], optional): a commitment to some unspecified + asset metadata (32 byte hash) + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + """ + def __init__(self, sender, sp, total, decimals, + default_frozen, *, + manager=None, reserve=None, freeze=None, clawback=None, + unit_name="", asset_name="", url="", + metadata_hash=None, + note=None, lease=None, rekey_to=None): + super().__init__(sender=sender, sp=sp, total=total, decimals=decimals, + default_frozen=default_frozen, + manager=manager, reserve=reserve, + freeze=freeze, clawback=clawback, + unit_name=unit_name, asset_name=asset_name, url=url, + metadata_hash=metadata_hash, + note=note, lease=lease, rekey_to=rekey_to, + strict_empty_address_check=False) + +class AssetDestroyTxn(AssetConfigTxn): + """Represents a transaction for asset destruction. + + An asset destruction transaction can only be sent by the manager + address, and only when the manager possseses all units of the + asset. + + """ + def __init__(self, sender, sp, index, + note=None, lease=None, rekey_to=None): + super().__init__(sender=sender, sp=sp, index=self.creatable_index(index), + note=note, lease=lease, rekey_to=rekey_to, + strict_empty_address_check=False) + +class AssetUpdateTxn(AssetConfigTxn): + """Represents a transaction for asset modification. + + To update asset configuration, include the following: + index, manager, reserve, freeze, clawback. + + Keyword arguments are required, starting with the special + addresses, to prevent argument reordinering errors. Since the + special addresses are required, strict_empty_address_check is + turned off. + + Args: + sender (str): address of the sender + sp (SuggestedParams): suggested params from algod + index (int): index of the asset to reconfigure + manager (str): address allowed to change nonzero addresses + for this asset + reserve (str): account whose holdings of this asset should + be reported as "not minted" + freeze (str): account allowed to change frozen state of + holdings of this asset + clawback (str): account allowed take units of this asset + from any account + note (bytes, optional): arbitrary optional bytes + lease (byte[32], optional): specifies a lease, and no other transaction + with the same sender and lease can be confirmed in this + transaction's valid rounds + rekey_to (str, optional): additionally rekey the sender to this address + + """ + def __init__(self, sender, sp, index, *, + manager, reserve, freeze, clawback, + note=None, lease=None, rekey_to=None): + super().__init__(sender=sender, sp=sp, index=self.creatable_index(index), + manager=manager, reserve=reserve, + freeze=freeze, clawback=clawback, + note=note, lease=lease, rekey_to=rekey_to, + strict_empty_address_check=False) + class AssetFreezeTxn(Transaction): + """ Represents a transaction for freezing or unfreezing an account's asset holdings. Must be issued by the asset's freeze manager. @@ -677,7 +862,7 @@ def __init__(self, sender, sp, index, target, new_freeze_state, note=None, lease=None, rekey_to=None): Transaction.__init__(self, sender, sp, note, lease, constants.assetfreeze_txn, rekey_to) - self.index = index + self.index = self.creatable_index(index, required=True) self.target = target self.new_freeze_state = new_freeze_state if sp.flat_fee: @@ -726,7 +911,7 @@ class AssetTransferTxn(Transaction): Represents a transaction for asset transfer. To begin accepting an asset, supply the same address as both sender and - receiver, and set amount to 0. + receiver, and set amount to 0 (or use AssetOptInTxn) To revoke an asset, set revocation_target, and issue the transaction from the asset's revocation manager account. @@ -778,7 +963,7 @@ def __init__(self, sender, sp, receiver, amt, index, self.amount = amt if (not isinstance(self.amount, int)) or self.amount < 0: raise error.WrongAmountType - self.index = index + self.index = self.creatable_index(index, required=True) self.close_assets_to = close_assets_to self.revocation_target = revocation_target if sp.flat_fee: @@ -837,6 +1022,52 @@ def __eq__(self, other): self.revocation_target == other.revocation_target) +class AssetOptInTxn(AssetTransferTxn): + """ + Make a transaction that will opt in to an ASA + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + index (int): the ASA to opt into + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + + Attributes: + See AssetTransferTxn + """ + + def __init__(self, sender, sp, index, + note=None, lease=None, rekey_to=None): + super().__init__(sender=sender, sp=sp, receiver=sender, amt=0, + index=index, note=note, lease=lease, rekey_to=rekey_to) + + +class AssetCloseOutTxn(AssetTransferTxn): + """ + Make a transaction that will send all of an ASA away, and opt out of it. + + Args: + sender (str): address of sender + sp (SuggestedParams): contains information such as fee and genesis hash + receiver (str): address of the receiver + index (int): the ASA to opt into + note(bytes, optional): transaction note field + lease(bytes, optional): transaction lease field + rekey_to(str, optional): rekey-to field, see Transaction + + Attributes: + See AssetTransferTxn + """ + + def __init__(self, sender, sp, receiver, index, + note=None, lease=None, rekey_to=None): + super().__init__(sender=sender, sp=sp, receiver=receiver, + amt=0, index=index, close_assets_to=receiver, + note=note, lease=lease, rekey_to=rekey_to) + + class StateSchema: """ Restricts state for an application call. @@ -953,22 +1184,62 @@ def __init__(self, sender, sp, index, note=None, lease=None, rekey_to=None): Transaction.__init__(self, sender, sp, note, lease, constants.appcall_txn, rekey_to) - self.index = index + self.index = self.creatable_index(index) self.on_complete = on_complete - self.local_schema = local_schema - self.global_schema = global_schema - self.approval_program = approval_program - self.clear_program = clear_program - self.app_args = app_args + self.local_schema = self.state_schema(local_schema) + self.global_schema = self.state_schema(global_schema) + self.approval_program = self.teal_bytes(approval_program) + self.clear_program = self.teal_bytes(clear_program) + self.app_args = self.bytes_list(app_args) self.accounts = accounts - self.foreign_apps = foreign_apps - self.foreign_assets = foreign_assets + self.foreign_apps = self.int_list(foreign_apps) + self.foreign_assets = self.int_list(foreign_assets) if sp.flat_fee: self.fee = max(constants.min_txn_fee, self.fee) else: self.fee = max(self.estimate_size() * self.fee, constants.min_txn_fee) + @staticmethod + def state_schema(schema): + """Confirm the argument is a StateSchema, or false which is coerced to None""" + if not schema or not schema.dictify(): + return None # Coerce false/empty values to None, to help __eq__ + assert isinstance(schema, StateSchema), f"{schema} is not a StateSchema" + return schema + + @staticmethod + def teal_bytes(teal): + """Confirm the argument is bytes-like, or false which is coerced to None""" + if not teal: + return None # Coerce false values like "" to None, to help __eq__ + assert isinstance(teal, (bytes, bytearray)), f"Program {teal} is not bytes" + return teal + + @staticmethod + def bytes_list(lst): + """Confirm or coerce list elements to bytes. Return None for empty/false lst. """ + def as_bytes(e): + if isinstance(e, (bytes, bytearray)): + return e + if isinstance(e, str): + return e.encode() + if isinstance(e, int): + # Uses 8 bytes, big endian to match TEAL's btoi + return e.to_bytes(8, "big") # raises for negative or too big + assert False, f"{e} is not bytes, str, or int" + + if not lst: + return None + return [as_bytes(elt) for elt in lst] + + @staticmethod + def int_list(lst): + """Confirm or coerce list elements to int. Return None for empty/false lst. """ + if not lst: + return None + return [int(elt) for elt in lst] + def dictify(self): d = dict() if self.index: @@ -1041,7 +1312,7 @@ class ApplicationCreateTxn(ApplicationCallTxn): approval_program (bytes): the compiled TEAL that approves a transaction clear_program (bytes): the compiled TEAL that runs when clearing state global_schema (StateSchema): restricts the number of ints and byte slices in the global state - local_schema (StateSchema): restructs the number of ints and byte slices in the per-user local state + local_schema (StateSchema): restricts the number of ints and byte slices in the per-user local state app_args(list[bytes], optional): any additional arguments to the application accounts(list[str], optional): any additional accounts to supply to the application foreign_apps(list[int], optional): any other apps used by the application, identified by app index @@ -1059,18 +1330,13 @@ def __init__(self, sender, sp, on_complete, approval_program, clear_program, glo app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=0, on_complete=on_complete, - approval_program=approval_program, clear_program=clear_program, + approval_program=self.required(approval_program), + clear_program=self.required(clear_program), global_schema=global_schema, local_schema=local_schema, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationCreateTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class ApplicationUpdateTxn(ApplicationCallTxn): """ @@ -1097,17 +1363,13 @@ class ApplicationUpdateTxn(ApplicationCallTxn): def __init__(self, sender, sp, index, approval_program, clear_program, app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): - ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=index, on_complete=OnComplete.UpdateApplicationOC, + ApplicationCallTxn.__init__(self, sender=sender, sp=sp, + index=self.creatable_index(index, required=True), + on_complete=OnComplete.UpdateApplicationOC, approval_program=approval_program, clear_program=clear_program, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationUpdateTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class ApplicationDeleteTxn(ApplicationCallTxn): """ @@ -1131,16 +1393,12 @@ class ApplicationDeleteTxn(ApplicationCallTxn): def __init__(self, sender, sp, index, app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): - ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=index, on_complete=OnComplete.DeleteApplicationOC, + ApplicationCallTxn.__init__(self, sender=sender, sp=sp, + index=self.creatable_index(index, required=True), + on_complete=OnComplete.DeleteApplicationOC, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationDeleteTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class ApplicationOptInTxn(ApplicationCallTxn): """ @@ -1163,16 +1421,12 @@ class ApplicationOptInTxn(ApplicationCallTxn): """ def __init__(self, sender, sp, index, app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): - ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=index, on_complete=OnComplete.OptInOC, + ApplicationCallTxn.__init__(self, sender=sender, sp=sp, + index=self.creatable_index(index, required=True), + on_complete=OnComplete.OptInOC, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationOptInTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class ApplicationCloseOutTxn(ApplicationCallTxn): """ @@ -1195,20 +1449,16 @@ class ApplicationCloseOutTxn(ApplicationCallTxn): """ def __init__(self, sender, sp, index, app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): - ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=index, on_complete=OnComplete.CloseOutOC, + ApplicationCallTxn.__init__(self, sender=sender, sp=sp, + index=self.creatable_index(index), + on_complete=OnComplete.CloseOutOC, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationCloseOutTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class ApplicationClearStateTxn(ApplicationCallTxn): """ - Make a transaction that will clear a user's state an application + Make a transaction that will clear a user's state for an application Args: sender (str): address of sender @@ -1227,16 +1477,12 @@ class ApplicationClearStateTxn(ApplicationCallTxn): """ def __init__(self, sender, sp, index, app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): - ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=index, on_complete=OnComplete.ClearStateOC, + ApplicationCallTxn.__init__(self, sender=sender, sp=sp, + index=self.creatable_index(index), + on_complete=OnComplete.ClearStateOC, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationClearStateTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class ApplicationNoOpTxn(ApplicationCallTxn): """ @@ -1260,16 +1506,12 @@ class ApplicationNoOpTxn(ApplicationCallTxn): """ def __init__(self, sender, sp, index, app_args=None, accounts=None, foreign_apps=None, foreign_assets=None, note=None, lease=None, rekey_to=None): - ApplicationCallTxn.__init__(self, sender=sender, sp=sp, index=index, on_complete=OnComplete.NoOpOC, + ApplicationCallTxn.__init__(self, sender=sender, sp=sp, + index=self.creatable_index(index), + on_complete=OnComplete.NoOpOC, app_args=app_args, accounts=accounts, foreign_apps=foreign_apps, foreign_assets=foreign_assets, note=note, lease=lease, rekey_to=rekey_to) - def dictify(self): - d = dict() - d.update(super(ApplicationNoOpTxn, self).dictify()) - od = OrderedDict(sorted(d.items())) - return od - class SignedTransaction: """ diff --git a/algosdk/logic.py b/algosdk/logic.py index 4948756e..1ead5801 100644 --- a/algosdk/logic.py +++ b/algosdk/logic.py @@ -34,6 +34,8 @@ def read_program(program, args=None): global spec, opcodes intcblock_opcode = 32 bytecblock_opcode = 38 + pushbytes_opcode = 128 + pushint_opcode = 129 if not program: raise error.InvalidProgram("empty program") @@ -76,10 +78,20 @@ def read_program(program, args=None): size = op['Size'] if size == 0: if op['Opcode'] == intcblock_opcode: - size_inc, ints = read_int_const_block(program, pc) + size_inc, found_ints = read_int_const_block(program, pc) + ints += found_ints size += size_inc elif op['Opcode'] == bytecblock_opcode: - size_inc, bytearrays = read_byte_const_block(program, pc) + size_inc, found_bytearrays = read_byte_const_block(program, pc) + bytearrays += found_bytearrays + size += size_inc + elif op['Opcode'] == pushint_opcode: + size_inc, found_int = read_push_int_block(program, pc) + ints.append(found_int) + size += size_inc + elif op['Opcode'] == pushbytes_opcode: + size_inc, found_bytearray = read_push_byte_block(program, pc) + bytearrays.append(found_bytearray) size += size_inc else: raise error.InvalidProgram("invalid instruction") @@ -137,12 +149,41 @@ def read_byte_const_block(program, pc): raise error.InvalidProgram( "could not decode []byte const[%d] at pc=%d" % (i, pc + size)) size += bytes_used - if pc + size >= len(program): + if pc + size + item_len > len(program): raise error.InvalidProgram("bytecblock ran past end of program") bytearrays.append(program[pc+size:pc+size+item_len]) size += item_len return size, bytearrays +def check_push_int_block(program, pc): + size, _ = read_push_int_block(program, pc) + return size + +def read_push_int_block(program, pc): + size = 1 + single_int, bytes_used = parse_uvarint(program[pc + size:]) + if bytes_used <= 0: + raise error.InvalidProgram( + "could not decode push int const at pc=%d" % (pc + size)) + size += bytes_used + return size, single_int + +def check_push_byte_block(program, pc): + size, _ = read_push_byte_block(program, pc) + return size + +def read_push_byte_block(program, pc): + size = 1 + item_len, bytes_used = parse_uvarint(program[pc + size:]) + if bytes_used <= 0: + raise error.InvalidProgram( + "could not decode push []byte const size at pc=%d" % (pc + size)) + size += bytes_used + if pc + size + item_len > len(program): + raise error.InvalidProgram("pushbytes ran past end of program") + single_bytearray = program[pc+size:pc+size+item_len] + size += item_len + return size, single_bytearray def parse_uvarint(buf): x = 0 diff --git a/algosdk/mnemonic.py b/algosdk/mnemonic.py index 14e9351b..ca4ce933 100644 --- a/algosdk/mnemonic.py +++ b/algosdk/mnemonic.py @@ -6,7 +6,17 @@ from . import encoding -word_list = wordlist.word_list_raw().split("\n") +word_to_index = {} +index_to_word = {} +for i, word in enumerate(wordlist.word_list_raw().split("\n")): + index_to_word[i] = word + # Put all prefixes of words at least four letters long into map, + # since they are guarenteed unique, so some people may only save + # that part, and expect to be able to recover. + for length in range(4, len(word)): + assert word[:length] not in word_to_index + word_to_index[word[:length]] = i + word_to_index[word] = i # in case word is less than four letters long def from_master_derivation_key(key): @@ -94,10 +104,10 @@ def _from_key(key): """ if not len(key) == constants.key_len_bytes: raise error.WrongKeyBytesLengthError - chksum = _checksum(key) + chkword = index_to_word[_checksum(key)] nums = _to_11_bit(key) words = _apply_words(nums) - return " ".join(words) + " " + chksum + return " ".join(words) + " " + chkword def _to_key(mnemonic): @@ -110,11 +120,14 @@ def _to_key(mnemonic): Returns: bytes: key """ - mnemonic = mnemonic.split(" ") + mnemonic = mnemonic.lower().split() if not len(mnemonic) == constants.mnemonic_len: raise error.WrongMnemonicLengthError - m_checksum = mnemonic[-1] - mnemonic = _from_words(mnemonic[:-1]) + try: + m_checksum = word_to_index[mnemonic[-1]] + mnemonic = _from_words(mnemonic[:-1]) + except KeyError: # We used to return ValueError, so keep it + raise ValueError(mnemonic) m_bytes = _to_bytes(mnemonic) if not m_bytes[-1:len(m_bytes)] == b'\x00': raise error.WrongChecksumError @@ -133,12 +146,12 @@ def _checksum(data): data (bytes): data to compute checksum of Returns: - bytes: checksum + int: checksum """ chksum = encoding.checksum(data) temp = chksum[0:2] nums = _to_11_bit(temp) - return _apply_words(nums)[0] + return nums[0] def _apply_words(nums): @@ -151,10 +164,7 @@ def _apply_words(nums): Returns: str[]: list of words """ - words = [] - for n in nums: - words.append(word_list[n]) - return words + return [index_to_word[n] for n in nums] def _from_words(words): @@ -167,10 +177,7 @@ def _from_words(words): Returns: int[]: list of 11-bit numbers """ - nums = [] - for w in words: - nums.append(word_list.index(w)) - return nums + return [word_to_index[w] for w in words] def _to_11_bit(data): diff --git a/algosdk/v2client/algod.py b/algosdk/v2client/algod.py index 67c6bd23..ae4ff7de 100644 --- a/algosdk/v2client/algod.py +++ b/algosdk/v2client/algod.py @@ -94,6 +94,26 @@ def account_info(self, address, **kwargs): """ req = "/accounts/" + address return self.algod_request("GET", req, **kwargs) + + def asset_info(self, asset_id, **kwargs): + """ + Return information about a specific asset. + + Args: + asset_id (int): The ID of the asset to look up. + """ + req = "/assets/" + str(asset_id) + return self.algod_request("GET", req, **kwargs) + + def application_info(self, application_id, **kwargs): + """ + Return information about a specific application. + + Args: + application_id (int): The ID of the application to look up. + """ + req = "/applications/" + str(application_id) + return self.algod_request("GET", req, **kwargs) def pending_transactions_by_address(self, address, limit=0, response_format="json", **kwargs): @@ -169,6 +189,8 @@ def send_transaction(self, txn, **kwargs): Returns: str: transaction ID """ + assert not isinstance(txn, future.transaction.Transaction), \ + f"Attempt to send UNSIGNED transaction {txn}" return self.send_raw_transaction(encoding.msgpack_encode(txn), **kwargs) @@ -243,6 +265,8 @@ def send_transactions(self, txns, **kwargs): """ serialized = [] for txn in txns: + assert not isinstance(txn, future.transaction.Transaction), \ + f"Attempt to send UNSIGNED transaction {txn}" serialized.append(base64.b64decode(encoding.msgpack_encode(txn))) return self.send_raw_transaction(base64.b64encode( @@ -297,6 +321,21 @@ def dryrun(self, drr, **kwargs): data = base64.b64decode(data) return self.algod_request("POST", req, data=data, headers=headers, **kwargs) + def genesis(self, **kwargs): + """Returns the entire genesis file.""" + req = "/genesis" + return self.algod_request("GET", req, **kwargs) + + def proof(self, round_num, txid, **kwargs): + """ + Get the proof for a given transaction in a round. + + Args: + round_num (int): The round in which the transaction appears. + txid (str): The transaction ID for which to generate a proof. + """ + req = "/blocks/{}/transactions/{}/proof".format(round_num, txid) + return self.algod_request("GET", req, **kwargs) def _specify_round_string(block, round_num): """ diff --git a/algosdk/v2client/indexer.py b/algosdk/v2client/indexer.py index f17f3257..74fcaa06 100644 --- a/algosdk/v2client/indexer.py +++ b/algosdk/v2client/indexer.py @@ -90,7 +90,7 @@ def health(self, **kwargs): def accounts( self, asset_id=None, limit=None, next_page=None, min_balance=None, max_balance=None, block=None, auth_addr=None, application_id=None, - round_num=None, **kwargs): + round_num=None, include_all=False, **kwargs): """ Return accounts that match the search; microalgos are the default currency unless asset_id is specified, in which case the asset will @@ -113,6 +113,10 @@ def accounts( application round_num (int, optional): alias for block; only specify one of these + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/accounts" query = dict() @@ -131,10 +135,12 @@ def accounts( query["auth-addr"] = auth_addr if application_id: query["application-id"] = application_id + if include_all: + query["include-all"] = include_all return self.indexer_request("GET", req, query, **kwargs) def asset_balances(self, asset_id, limit=None, next_page=None, min_balance=None, - max_balance=None, block=None, round_num=None, **kwargs): + max_balance=None, block=None, round_num=None, include_all=False, **kwargs): """ Return accounts that hold the asset; microalgos are the default currency unless asset_id is specified, in which case the asset will @@ -153,6 +159,10 @@ def asset_balances(self, asset_id, limit=None, next_page=None, min_balance=None, some configurations round_num (int, optional): alias for block; only specify one of these + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/assets/" + str(asset_id) + "/balances" query = dict() @@ -164,6 +174,8 @@ def asset_balances(self, asset_id, limit=None, next_page=None, min_balance=None, query["currency-greater-than"] = min_balance if max_balance: query["currency-less-than"] = max_balance + if include_all: + query["include-all"] = include_all _specify_round(query, block, round_num) return self.indexer_request("GET", req, query, **kwargs) @@ -182,7 +194,8 @@ def block_info(self, block=None, round_num=None, **kwargs): return self.indexer_request("GET", req, **kwargs) - def account_info(self, address, block=None, round_num=None, **kwargs): + def account_info(self, address, block=None, round_num=None, + include_all=False, **kwargs): """ Return account information. @@ -191,13 +204,30 @@ def account_info(self, address, block=None, round_num=None, **kwargs): block (int, optional): use results from the specified round round_num (int, optional): alias for block; only specify one of these + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/accounts/" + address query = dict() _specify_round(query, block, round_num) + if include_all: + query["include-all"] = include_all return self.indexer_request("GET", req, query, **kwargs) + def transaction(self, txid, **kwargs): + """ + Returns information about the given transaction. + + Args: + txid (str): The ID of the transaction to look up. + """ + req = "/transactions/" + txid + + return self.indexer_request("GET", req, **kwargs) + def search_transactions( self, limit=None, next_page=None, note_prefix=None, txn_type=None, sig_type=None, txid=None, block=None, min_round=None, max_round=None, @@ -464,7 +494,7 @@ def search_asset_transactions(self, asset_id, limit=None, next_page=None, note_p def search_assets( self, limit=None, next_page=None, creator=None, name=None, unit=None, - asset_id=None, **kwargs): + asset_id=None, include_all=False, **kwargs): """ Return assets that satisfy the conditions. @@ -477,6 +507,10 @@ def search_assets( name (str, optional): filter just assets with the given name unit (str, optional): filter just assets with the given unit asset_id (int, optional): return only the asset with this ID + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/assets" query = dict() @@ -492,49 +526,66 @@ def search_assets( query["unit"] = unit if asset_id: query["asset-id"] = asset_id + if include_all: + query["include-all"] = include_all return self.indexer_request("GET", req, query, **kwargs) - def asset_info(self, asset_id, **kwargs): + def asset_info(self, asset_id, include_all=False, **kwargs): """ Return asset information. Args: asset_id (int): asset index + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/assets/" + str(asset_id) - return self.indexer_request("GET", req, **kwargs) + query = dict() + if include_all: + query["include-all"] = include_all + return self.indexer_request("GET", req, query, **kwargs) - def applications( - self, application_id, round=None, round_num=None, **kwargs): + def applications(self, application_id, round=None, round_num=None, + include_all=False, **kwargs): """ Return applications that satisfy the conditions. Args: application_id (int): application index - round (int, optional): restrict search to passed round; - deprecated, please use round_num - round_num (int, optional): alias for round; only specify one of these + round (int, optional): not supported, DO NOT USE! + round_num (int, optional): not supported, DO NOT USE! + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/applications/" + str(application_id) query = dict() _specify_round(query, round, round_num) + if include_all: + query["include-all"] = include_all return self.indexer_request("GET", req, query, **kwargs) def search_applications( self, application_id=None, round=None, limit=None, next_page=None, - round_num=None, **kwargs): + round_num=None, include_all=False, **kwargs): """ Return applications that satisfy the conditions. Args: application_id (int, optional): restrict search to application index - round (int, optional): restrict search to passed round - deprecated, please use round_num + round (int, optional): not supported, DO NOT USE! limit (int, optional): restrict number of results to limit next_page (string, optional): used for pagination - round_num (int, optional): alias for round; only specify one of these + round_num (int, optional): not supported, DO NOT USE! + include_all (bool, optional): include all items including closed + accounts, deleted applications, destroyed assets, opted-out + asset holdings, and closed-out application localstates. Defaults + to false. """ req = "/applications" query = dict() @@ -545,6 +596,8 @@ def search_applications( query["limit"] = limit if next_page: query["next"] = next_page + if include_all: + query["include-all"] = include_all return self.indexer_request("GET", req, query, **kwargs) diff --git a/setup.py b/setup.py index 35450982..e6140f45 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Algorand SDK in Python", author="Algorand", author_email="pypiservice@algorand.com", - version="1.4.1", + version="1.5.0", long_description=long_description, long_description_content_type="text/markdown", license="MIT", diff --git a/test/steps/v2_steps.py b/test/steps/v2_steps.py index a721ad14..6cdc21b9 100644 --- a/test/steps/v2_steps.py +++ b/test/steps/v2_steps.py @@ -14,7 +14,7 @@ from algosdk.v2client import * from algosdk.v2client.models import DryrunRequest, DryrunSource, \ Account, Application, ApplicationLocalState -from algosdk.error import AlgodHTTPError +from algosdk.error import AlgodHTTPError, IndexerHTTPError from algosdk.testing.dryrun import DryrunTestCaseMixin from test.steps.steps import token as daemon_token @@ -27,6 +27,13 @@ def parse_string(text): register_type(MaybeString=parse_string) +@parse.with_pattern(r"true|false") +def parse_bool(value): + if value not in ("true", "false"): + raise ValueError("Unknown value for include_all: {}".format(value)) + return value == "true" + +register_type(MaybeBool=parse_bool) @given("mock server recording request paths") def setup_mockserver(context): @@ -218,6 +225,13 @@ def acc_info_any(context): def parse_acc_info(context, address): assert context.response["address"] == address +@when('we make a GetAssetByID call for assetID {asset_id}') +def asset_info(context, asset_id): + context.response = context.acl.asset_info(int(asset_id)) + +@when('we make a GetApplicationByID call for applicationID {app_id}') +def application_info(context, app_id): + context.response = context.acl.application_info(int(app_id)) @when('we make a Get Block call against block number {block} with format "{response_format}"') def block(context, block, response_format): @@ -579,6 +593,13 @@ def lookup_asset_any(context): def parse_asset(context, index): assert context.response["asset"]["index"] == int(index) +@when('we make a LookupApplications call with applicationID {app_id}') +def lookup_application(context, app_id): + context.response = context.icl.applications(int(app_id)) + +@when('we make a SearchForApplications call with applicationID {app_id}') +def search_application(context, app_id): + context.response = context.icl.search_applications(int(app_id)) @when( 'we make a Search Accounts call with assetID {index} limit {limit} currencyGreaterThan {currencyGreaterThan} currencyLessThan {currencyLessThan} and round {block}') @@ -597,6 +618,16 @@ def search_accounts(context, index, limit, currencyGreaterThan, currencyLessThan min_balance=int(currencyGreaterThan), max_balance=int(currencyLessThan), block=int(block), auth_addr=authAddr) +@when( + 'I use {indexer} to search for an account with {assetid}, {limit}, {currencygt}, {currencylt}, "{auth_addr:MaybeString}", {application_id}, "{include_all:MaybeBool}" and token "{token:MaybeString}"') +def icl_search_accounts_with_auth_addr_and_app_id_and_include_all(context, indexer, assetid, limit, currencygt, currencylt, auth_addr, application_id, include_all, token): + context.response = context.icls[indexer].accounts(asset_id=int(assetid), limit=int(limit), next_page=token, + min_balance=int(currencygt), + max_balance=int(currencylt), + auth_addr=auth_addr, + application_id=int(application_id), + include_all=include_all) + @when( 'I use {indexer} to search for an account with {assetid}, {limit}, {currencygt}, {currencylt}, "{auth_addr:MaybeString}", {application_id} and token "{token:MaybeString}"') def icl_search_accounts_with_auth_addr_and_app_id(context, indexer, assetid, limit, currencygt, currencylt, auth_addr, application_id, token): @@ -606,7 +637,6 @@ def icl_search_accounts_with_auth_addr_and_app_id(context, indexer, assetid, lim auth_addr=auth_addr, application_id=int(application_id)) - @when( 'I use {indexer} to search for an account with {assetid}, {limit}, {currencygt}, {currencylt} and token "{token:MaybeString}"') def icl_search_accounts_legacy(context, indexer, assetid, limit, currencygt, currencylt, token): @@ -906,24 +936,33 @@ def check_assets(context, num, assetidout): if int(num) > 0: assert context.response["assets"][0]["index"] == int(assetidout) +@when('I use {indexer} to search for applications with {limit}, {application_id}, "{include_all:MaybeBool}" and token "{token:MaybeString}"') +def search_applications_include_all(context, indexer, limit, application_id, include_all, token): + context.response = context.icls[indexer].search_applications(application_id=int(application_id),limit=int(limit), + include_all=include_all,next_page=token) @when('I use {indexer} to search for applications with {limit}, {application_id}, and token "{token:MaybeString}"') -def step_impl(context, indexer, limit, application_id, token): +def search_applications(context, indexer, limit, application_id, token): context.response = context.icls[indexer].search_applications(application_id=int(application_id),limit=int(limit), next_page=token) +@when('I use {indexer} to lookup application with {application_id} and "{include_all:MaybeBool}"') +def lookup_application_include_all(context, indexer, application_id, include_all): + try: + context.response = context.icls[indexer].applications(application_id=int(application_id), include_all=include_all) + except IndexerHTTPError as e: + context.response = json.loads(str(e)) @when('I use {indexer} to lookup application with {application_id}') -def step_impl(context, indexer, application_id): +def lookup_application(context, indexer, application_id): context.response = context.icls[indexer].applications(application_id=int(application_id)) - @then(u'the parsed response should equal "{jsonfile}".') def step_impl(context, jsonfile): loaded_response = None dir_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.dirname(os.path.dirname(dir_path)) - with open(dir_path + "/test-harness/features/resources/" + jsonfile, "rb") as f: + with open(dir_path + "/test/features/resources/" + jsonfile, "rb") as f: loaded_response = bytearray(f.read()) # sort context.response def recursively_sort_on_key(dictionary): @@ -1085,12 +1124,12 @@ def build_app_transaction(context, operation, application_id, sender, approval_p if approval_program == "none": approval_program = None elif approval_program: - with open(dir_path + "/test-harness/features/resources/" + approval_program, "rb") as f: + with open(dir_path + "/test/features/resources/" + approval_program, "rb") as f: approval_program = bytearray(f.read()) if clear_program == "none": clear_program = None elif clear_program: - with open(dir_path + "/test-harness/features/resources/" + clear_program, "rb") as f: + with open(dir_path + "/test/features/resources/" + clear_program, "rb") as f: clear_program = bytearray(f.read()) if app_args == "none": app_args = None @@ -1172,12 +1211,12 @@ def build_app_txn_with_transient(context, operation, approval_program, clear_pro if approval_program == "none": approval_program = None elif approval_program: - with open(dir_path + "/test-harness/features/resources/" + approval_program, "rb") as f: + with open(dir_path + "/test/features/resources/" + approval_program, "rb") as f: approval_program = bytearray(f.read()) if clear_program == "none": clear_program = None elif clear_program: - with open(dir_path + "/test-harness/features/resources/" + clear_program, "rb") as f: + with open(dir_path + "/test/features/resources/" + clear_program, "rb") as f: clear_program = bytearray(f.read()) if int(local_ints) == 0 and int(local_bytes) == 0: local_schema = None diff --git a/test_unit.py b/test_unit.py index 8eeeb57c..ff7c1834 100644 --- a/test_unit.py +++ b/test_unit.py @@ -1,24 +1,18 @@ import base64 import copy -import unittest import random +import unittest from unittest.mock import Mock -from algosdk.future import transaction -from algosdk import encoding -from algosdk import account -from algosdk import mnemonic -from algosdk import wordlist -from algosdk import error -from algosdk import constants -from algosdk import util -from algosdk import logic -from algosdk.future import template +from nacl.signing import SigningKey + +from algosdk import (account, constants, encoding, error, logic, mnemonic, + util, wordlist) +from algosdk.future import template, transaction from algosdk.testing import dryrun -from nacl.signing import SigningKey -class TestTransaction(unittest.TestCase): +class TestPaymentTransaction(unittest.TestCase): def test_min_txn_fee(self): address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" @@ -32,17 +26,39 @@ def test_note_wrong_type(self): gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" sp = transaction.SuggestedParams(0, 1, 100, gh) f = lambda: transaction.PaymentTxn(address, sp, address, - 1000, note="hello") + 1000, note=45) self.assertRaises(error.WrongNoteType, f) + def test_note_strings_allowed(self): + address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" + gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" + sp = transaction.SuggestedParams(0, 1, 100, gh) + txn = transaction.PaymentTxn(address, sp, address, + 1000, note="helo") + self.assertEqual(constants.min_txn_fee, txn.fee) + def test_note_wrong_length(self): address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" sp = transaction.SuggestedParams(0, 1, 100, gh) - f = lambda: transaction.PaymentTxn(address, sp, address, + f = lambda: transaction.PaymentTxn(address, sp, address, 1000, note=("0"*1025).encode()) self.assertRaises(error.WrongNoteLength, f) + def test_leases(self): + address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" + gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" + sp = transaction.SuggestedParams(0, 1, 100, gh) + # 32 byte zero lease should be dropped from msgpack + txn1 = transaction.PaymentTxn(address, sp, address, + 1000, lease=(b"\0"*32)) + txn2 = transaction.PaymentTxn(address, sp, address, + 1000) + + self.assertEqual(txn1.dictify(), txn2.dictify()) + self.assertEqual(txn1, txn2) + self.assertEqual(txn2, txn1) + def test_serialize(self): address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" @@ -154,7 +170,7 @@ def test_sign(self): self.assertEqual(enc, re_enc) def test_sign_logic_multisig(self): - program = b"\x01\x20\x01\x01\x22" + program = b"\x01\x20\x01\x01\x22" lsig = transaction.LogicSig(program) passphrase = "sight garment riot tattoo tortoise identify left talk sea ill walnut leg robot myth toe perfect rifle dizzy spend april build legend brother above hospital" sk = mnemonic.to_private_key(passphrase) @@ -166,7 +182,7 @@ def test_sign_logic_multisig(self): msig = transaction.Multisig(1, 2, [addr, addr2]) lsig.sign(sk, msig) - lsig.append_to_multisig(sk2) + lsig.append_to_multisig(sk2) receiver = "DOMUC6VGZH7SSY5V332JR5HRLZSOJDWNPBI4OI2IIBU6A3PFLOBOXZ3KFY" gh = "zNQES/4IqimxRif40xYvzBBIYCZSbYvNSRIzVIh4swo=" @@ -187,7 +203,7 @@ def test_sign_logic_multisig(self): "2XuuO6mS+3IetwlKVPM0qdKBIiMVdhzAOMPKpHR5cGWjcGF5") encoded = encoding.msgpack_encode(lstx) - self.assertEquals(encoded, golden) + self.assertEqual(encoded, golden) def test_serialize_zero_receiver(self): address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" @@ -201,7 +217,6 @@ def test_serialize_zero_receiver(self): "iKNhbXTNA+ijZmVlzQPoomZ2AaJnaMQgJgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMe" "K+wRSaQ7dKibHZkpG5vdGXEAwEgyKNzbmTEIP5oQQPnKvM7kbGuuSOunAVfSbJzHQ" "tAtCP3Bf2XdDxmpHR5cGWjcGF5") - print(encoding.msgpack_encode(txn)) self.assertEqual(golden, encoding.msgpack_encode(txn)) @@ -210,16 +225,16 @@ def test_error_empty_receiver_txn(self): receiver = None gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" sp = transaction.SuggestedParams(3, 1, 100, gh) - + with self.assertRaises(error.ZeroAddressError): transaction.PaymentTxn(address, sp, receiver, 1000) - + def test_error_empty_receiver_asset_txn(self): address = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" receiver = None gh = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" sp = transaction.SuggestedParams(3, 1, 100, gh) - + with self.assertRaises(error.ZeroAddressError): transaction.AssetTransferTxn(address, sp, receiver, 1000, 24) @@ -289,7 +304,7 @@ def test_serialize_pay_lease(self): self.assertEqual(golden, encoding.msgpack_encode(signed_txn)) - def test_serialize_keyreg(self): + def test_serialize_keyreg_online(self): mn = ( "awful drop leaf tennis indoor begin mandate discover uncle seven " "only coil atom any hospital uncover make any climb actor armed me" @@ -320,6 +335,58 @@ def test_serialize_keyreg(self): "Vsc3TNJ38=") self.assertEqual(golden, encoding.msgpack_encode(signed_txn)) + def test_serialize_keyreg_offline(self): + mn = ( + "awful drop leaf tennis indoor begin mandate discover uncle seven " + "only coil atom any hospital uncover make any climb actor armed " + "measure need above hundred") + sk = mnemonic.to_private_key(mn) + pk = mnemonic.to_public_key(mn) + fee = 1000 + gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + votepk = None + selpk = None + votefirst = None + votelast = None + votedilution = None + + sp = transaction.SuggestedParams( + fee, 12299691, 12300691, gh, flat_fee=True) + txn = transaction.KeyregTxn(pk, sp, votepk, selpk, votefirst, votelast, + votedilution) + signed_txn = txn.sign(sk) + + golden = ( + "gqNzaWfEQJosTMSKwGr+eWN5XsAJvbjh2DkzOtEN6lrDNM4TAnYIjl9L43zU70gAX" + "USAehZo9RyejgDA12B75SR6jIdhzQCjdHhuhqNmZWXNA+iiZnbOALutq6JnaMQgSG" + "O1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOALuxk6NzbmTEIAn70nYs" + "CPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pHR5cGWma2V5cmVn") + self.assertEqual(golden, encoding.msgpack_encode(signed_txn)) + + def test_serialize_keyreg_nonpart(self): + mn = ( + "awful drop leaf tennis indoor begin mandate discover uncle seven " + "only coil atom any hospital uncover make any climb actor armed " + "measure need above hundred") + sk = mnemonic.to_private_key(mn) + pk = mnemonic.to_public_key(mn) + fee = 1000 + gh = "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=" + nonpart = True + + sp = transaction.SuggestedParams( + fee, 12299691, 12300691, gh, flat_fee=True) + txn = transaction.KeyregTxn(pk, sp, None, None, None, None, None, + nonpart=nonpart) + signed_txn = txn.sign(sk) + + golden = ( + "gqNzaWfEQN7kw3tLcC1IweQ2Ru5KSqFS0Ba0cn34ncOWPIyv76wU8JPLxyS8alErm4" + "PHg3Q7n1Mfqa9SQ9zDY+FMeZLLgQyjdHhuh6NmZWXNA+iiZnbOALutq6JnaMQgSGO1" + "GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOALuxk6dub25wYXJ0w6Nzbm" + "TEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tuH9pHR5cGWma2V5cmVn") + self.assertEqual(golden, encoding.msgpack_encode(signed_txn)) + def test_serialize_asset_create(self): mn = ( "awful drop leaf tennis indoor begin mandate discover uncle seven " @@ -713,7 +780,194 @@ def test_group_id(self): self.assertEqual(len(txns), 0) + +class TestAssetConfigConveniences(unittest.TestCase): + """Tests that the simplified versions of Config are equivalent to Config""" + sender = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" + genesis = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" + params = transaction.SuggestedParams(0, 1, 100, genesis) + + def test_asset_create(self): + create = transaction.AssetCreateTxn(self.sender, self.params, + 1000, "2", False, + manager=None, + reserve=None, + freeze=None, + clawback=None, + unit_name="NEWCOIN", + asset_name="A new kind of coin", + url="https://newcoin.co/") + config = transaction.AssetConfigTxn(self.sender, self.params, index=None, + total="1000", decimals=2, + unit_name="NEWCOIN", + asset_name="A new kind of coin", + url="https://newcoin.co/", + strict_empty_address_check=False) + self.assertEqual(create.dictify(), config.dictify()) + self.assertEqual(config, create) + + self.assertEqual(transaction.AssetCreateTxn.undictify(create.dictify()), + config) + + + def test_asset_update(self): + update = transaction.AssetUpdateTxn(self.sender, self.params, 6, + manager=None, + reserve=self.sender, + freeze=None, + clawback=None) + config = transaction.AssetConfigTxn(self.sender, self.params, index="6", + reserve=self.sender, + strict_empty_address_check=False) + self.assertEqual(update.dictify(), config.dictify()) + self.assertEqual(config, update) + + self.assertEqual(transaction.AssetUpdateTxn.undictify(update.dictify()), + config) + + def test_asset_destroy(self): + destroy = transaction.AssetDestroyTxn(self.sender, self.params, 23) + config = transaction.AssetConfigTxn(self.sender, self.params, index="23", + strict_empty_address_check=False) + self.assertEqual(destroy.dictify(), config.dictify()) + self.assertEqual(config, destroy) + + self.assertEqual(transaction.AssetDestroyTxn.undictify(destroy.dictify()), + config) + +class TestAssetTransferConveniences(unittest.TestCase): + sender = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" + receiver = "DOMUC6VGZH7SSY5V332JR5HRLZSOJDWNPBI4OI2IIBU6A3PFLOBOXZ3KFY" + genesis = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" + params = transaction.SuggestedParams(0, 1, 100, genesis) + def test_asset_optin(self): + optin = transaction.AssetOptInTxn(self.sender, self.params, "7") + xfer = transaction.AssetTransferTxn(self.sender, self.params, self.sender, + 0, index=7) + self.assertEqual(optin.dictify(), xfer.dictify()) + self.assertEqual(xfer, optin) + + self.assertEqual(transaction.AssetOptInTxn.undictify(optin.dictify()), + xfer) + + def test_asset_closeout(self): + closeout = transaction.AssetCloseOutTxn(self.sender, self.params, + self.receiver, "7") + xfer = transaction.AssetTransferTxn(self.sender, self.params, self.receiver, + 0, index=7, close_assets_to=self.receiver) + self.assertEqual(closeout.dictify(), xfer.dictify()) + self.assertEqual(xfer, closeout) + + self.assertEqual(transaction.AssetCloseOutTxn.undictify(closeout.dictify()), + xfer) + +class TestApplicationTransactions(unittest.TestCase): + sender = "7ZUECA7HFLZTXENRV24SHLU4AVPUTMTTDUFUBNBD64C73F3UHRTHAIOF6Q" + genesis = "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=" + lschema = transaction.StateSchema(1, 2) + gschema = transaction.StateSchema(3, 4) + + def test_application_call(self): + params = transaction.SuggestedParams(0, 1, 100, self.genesis) + for oc in transaction.OnComplete: + b = transaction.ApplicationCallTxn(self.sender, params, 10, oc, + app_args=[b"hello"]) + s = transaction.ApplicationCallTxn(self.sender, params, "10", oc, + app_args=["hello"]) + self.assertEqual(b, s) # string is encoded same as corresponding bytes + transaction.ApplicationCallTxn(self.sender, params, 10, oc, + app_args=[2,3,0]) # ints work + with self.assertRaises(AssertionError): + transaction.ApplicationCallTxn(self.sender, params, 10, oc, + app_args=[3.4]) # floats don't + with self.assertRaises(OverflowError): + transaction.ApplicationCallTxn(self.sender, params, 10, oc, + app_args=[-10]) # nor negative + transaction.ApplicationCallTxn(self.sender, params, 10, oc, # maxuint64 + app_args=[18446744073709551615]) + with self.assertRaises(OverflowError): + transaction.ApplicationCallTxn(self.sender, params, 10, oc, # too big + app_args=[18446744073709551616]) + + i = transaction.ApplicationCallTxn(self.sender, params, 10, oc, + foreign_apps=[4, 3], + foreign_assets=(2,1)) + s = transaction.ApplicationCallTxn(self.sender, params, "10", oc, + foreign_apps=["4", 3], + foreign_assets=[2, "1"]) + self.assertEqual(i, s) # string is encoded same as corresponding int + + def test_application_create(self): + approve = b"\0" + clear = b"\1" + params = transaction.SuggestedParams(0, 1, 100, self.genesis) + for oc in transaction.OnComplete: + # We will confirm that the Create is just shorthand for + # the Call. But note that the programs come before the + # schemas and the schemas are REVERSED! That's + # unfortunate, and we should consider adding "*" to the + # argument list after on_completion, thereby forcing the + # use of named arguments. + create = transaction.ApplicationCreateTxn(self.sender, params, oc, + approve, clear, + self.lschema, self.gschema) + call = transaction.ApplicationCallTxn(self.sender, params, 0, oc, + self.gschema, self.lschema, + approve, clear) + # Check the dict first, it's important on it's own, and it + # also gives more a meaningful error if they're not equal. + self.assertEqual(create.dictify(), call.dictify()) + self.assertEqual(create, call) + self.assertEqual(call, create) + + def test_application_create_schema(self): + approve = b"\0" + clear = b"\1" + zero_schema = transaction.StateSchema(0, 0) + params = transaction.SuggestedParams(0, 1, 100, self.genesis) + for oc in transaction.OnComplete: + # verify that a schema with 0 uints and 0 bytes behaves the same as no schema + txn_zero_schema = transaction.ApplicationCreateTxn(self.sender, params, oc, + approve, clear, + zero_schema, zero_schema) + txn_none_schema = transaction.ApplicationCreateTxn(self.sender, params, oc, + approve, clear, + None, None) + # Check the dict first, it's important on its own, and it + # also gives more a meaningful error if they're not equal. + self.assertEqual(txn_zero_schema.dictify(), txn_none_schema.dictify()) + self.assertEqual(txn_zero_schema, txn_none_schema) + self.assertEqual(txn_none_schema, txn_zero_schema) + + def test_application_update(self): + empty = b"" + params = transaction.SuggestedParams(0, 1, 100, self.genesis) + i = transaction.ApplicationUpdateTxn(self.sender, params, 10, empty, empty) + s = transaction.ApplicationUpdateTxn(self.sender, params, "10", empty, empty) + self.assertEqual(i, s) # int and string encoded same + + call = transaction.ApplicationCallTxn(self.sender, params, 10, + transaction.OnComplete.UpdateApplicationOC, + None, None, + empty, empty) + self.assertEqual(i.dictify(), call.dictify()) + self.assertEqual(i, call) + + def test_application_delete(self): + params = transaction.SuggestedParams(0, 1, 100, self.genesis) + i = transaction.ApplicationDeleteTxn(self.sender, params, 10) + s = transaction.ApplicationDeleteTxn(self.sender, params, "10") + self.assertEqual(i, s) # int and string encoded same + + call = transaction.ApplicationCallTxn(self.sender, params, 10, + transaction.OnComplete.DeleteApplicationOC) + self.assertEqual(i.dictify(), call.dictify()) + self.assertEqual(i, call) + class TestMnemonic(unittest.TestCase): + zero_bytes = bytes([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]) + def test_mnemonic_private_key(self): priv_key, _ = account.generate_account() mn = mnemonic.from_private_key(priv_key) @@ -721,17 +975,45 @@ def test_mnemonic_private_key(self): self.assertEqual(priv_key, mnemonic.to_private_key(mn)) def test_zero_mnemonic(self): - zero_bytes = bytes([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]) expected_mnemonic = ( "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon abandon abandon abandon " "abandon abandon abandon abandon abandon abandon abandon abandon " "invest") - result = mnemonic._from_key(zero_bytes) + result = mnemonic._from_key(self.zero_bytes) self.assertEqual(expected_mnemonic, result) result = mnemonic._to_key(result) - self.assertEqual(zero_bytes, result) + self.assertEqual(self.zero_bytes, result) + + def test_whitespace_irrelevance(self): + padded = """ + abandon abandon abandon abandon abandon abandon abandon abandon + abandon abandon abandon abandon abandon abandon abandon abandon + abandon abandon abandon abandon abandon abandon abandon abandon + invest + """ + result = mnemonic._to_key(padded) + self.assertEqual(self.zero_bytes, result) + + def test_case_irrelevance(self): + padded = """ + abandon ABANDON abandon abandon abandon abandon abandon abandon + abandon abandon abandon abandon abandon abandon abandon abandon + abandon abandon abandon abandon abandon abandon abandon abandon + invEST + """ + result = mnemonic._to_key(padded) + self.assertEqual(self.zero_bytes, result) + + def test_short_words(self): + padded = """ + aban abandon abandon abandon abandon abandon abandon abandon + aban abandon abandon abandon abandon abandon abandon abandon + aban abandon abandon abandon abandon abandon abandon abandon + inve + """ + result = mnemonic._to_key(padded) + self.assertEqual(self.zero_bytes, result) def test_wrong_checksum(self): mn = ( @@ -748,10 +1030,20 @@ def test_word_not_in_list(self): "abandon abandon abandon venues abandon abandon abandon abandon " "invest") self.assertRaises(ValueError, mnemonic._to_key, mn) + mn = ( + "abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon abandon abandon abandon abandon " + "x-ray") + self.assertRaises(ValueError, mnemonic._to_key, mn) - def test_wordlist(self): + def test_wordlist_integrity(self): + """This isn't a test of _checksum, it reminds us not to change the + wordlist. + + """ result = mnemonic._checksum(bytes(wordlist.word_list_raw(), "utf-8")) - self.assertEqual(result, "venue") + self.assertEqual(result, 1939) def test_mnemonic_wrong_len(self): mn = "abandon abandon abandon" @@ -997,7 +1289,7 @@ def test_multisig_txn(self): self.assertEqual(msigtxn, encoding.msgpack_encode( encoding.msgpack_decode(msigtxn))) - def test_keyreg_txn(self): + def test_keyreg_txn_online(self): keyregtxn = ( "jKNmZWXNA+iiZnbNcoqjZ2Vuq25ldHdvcmstdjM4omdoxCBN/+nfiNPXLbuigk8M/" "TXsMUfMK7dV//xB1wkoOhNu9qJsds1y7qZzZWxrZXnEIBguZEIjiD6KAPJq76B0ch" @@ -1007,6 +1299,26 @@ def test_keyreg_txn(self): self.assertEqual(keyregtxn, encoding.msgpack_encode( encoding.msgpack_decode(keyregtxn))) + def test_keyreg_txn_offline(self): + keyregtxn = ( + "hqNmZWXNA+iiZnbOALutq6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/c" + "OUJOiKibHbOALuxk6NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUOB+jFx2mGR9tu" + "H9pHR5cGWma2V5cmVn") + # using future_msgpack_decode instead of msgpack_decode + # because non-future transactions do not support offline keyreg + self.assertEqual(keyregtxn, encoding.msgpack_encode( + encoding.future_msgpack_decode(keyregtxn))) + + def test_keyreg_txn_nonpart(self): + keyregtxn = ( + "h6NmZWXNA+iiZnbOALutq6JnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/c" + "OUJOiKibHbOALuxk6dub25wYXJ0w6NzbmTEIAn70nYsCPhsWua/bdenqQHeZnXXUO" + "B+jFx2mGR9tuH9pHR5cGWma2V5cmVn") + # using future_msgpack_decode instead of msgpack_decode + # because non-future transactions do not support nonpart keyreg + self.assertEqual(keyregtxn, encoding.msgpack_encode( + encoding.future_msgpack_decode(keyregtxn))) + def test_asset_create(self): golden = ( "gqNzaWfEQEDd1OMRoQI/rzNlU4iiF50XQXmup3k5czI9hEsNqHT7K4KsfmA/0DUVk" @@ -1145,6 +1457,16 @@ def test_parse_bytecblock(self): size = logic.check_byte_const_block(data, 0) self.assertEqual(size, len(data)) + def test_parse_pushint(self): + data = b"\x81\x80\x80\x04" + size = logic.check_push_int_block(data, 0) + self.assertEqual(size, len(data)) + + def test_parse_pushbytes(self): + data = b"\x80\x0b\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64" + size = logic.check_push_byte_block(data, 0) + self.assertEqual(size, len(data)) + def test_check_program(self): program = b"\x01\x20\x01\x01\x22" # int 1 self.assertTrue(logic.check_program(program, None)) @@ -1180,6 +1502,7 @@ def test_check_program(self): with self.assertRaises(error.InvalidProgram): logic.check_program(program, []) + def test_check_program_teal_2(self): # check TEAL v2 opcodes self.assertIsNotNone(logic.spec, "Must be called after any of logic.check_program") self.assertTrue(logic.spec['EvalMaxVersion'] >= 2) @@ -1196,6 +1519,28 @@ def test_check_program(self): # asset_holding_get program = b"\x02\x20\x01\x00\x22\x22\x70\x00" # int 0; int 0; asset_holding_get Balance self.assertTrue(logic.check_program(program, None)) + + def test_check_program_teal_3(self): + # check TEAL v2 opcodes + self.assertIsNotNone(logic.spec, "Must be called after any of logic.check_program") + self.assertTrue(logic.spec['EvalMaxVersion'] >= 3) + self.assertTrue(logic.spec['LogicSigVersion'] >= 3) + + # min_balance + program = b"\x03\x20\x01\x00\x22\x78" # int 0; min_balance + self.assertTrue(logic.check_program(program, None)) + + # pushbytes + program = b"\x03\x20\x01\x00\x22\x80\x02\x68\x69\x48" # int 0; pushbytes "hi"; pop + self.assertTrue(logic.check_program(program, None)) + + # pushint + program = b"\x03\x20\x01\x00\x22\x81\x01\x48" # int 0; pushint 1; pop + self.assertTrue(logic.check_program(program, None)) + + # swap + program = b"\x03\x20\x02\x00\x01\x22\x23\x4c\x48" # int 0; int 1; swap; pop + self.assertTrue(logic.check_program(program, None)) def test_teal_sign(self): """test tealsign""" @@ -2025,7 +2370,10 @@ def test_local_state(self): if __name__ == "__main__": to_run = [ - TestTransaction, + TestPaymentTransaction, + TestAssetConfigConveniences, + TestAssetTransferConveniences, + TestApplicationTransactions, TestMnemonic, TestAddress, TestMultisig,