Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vm: add start_index to iterator unwrap helpers #298

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions neo3/api/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,24 +1206,31 @@ def process(res: noderpc.ExecutionResult, _: int = 0) -> list[bytes]:

return ContractMethodResult(sb.to_array(), process)

def tokens(self, limit: int = 2000) -> ContractMethodResult[list[bytes]]:
def tokens(
self, limit: int = 2000, start_index: int = 0
) -> ContractMethodResult[list[bytes]]:
"""
Get all tokens minted by the contract.

limit: the maximum tokens to return. Note: there is a limit on the virtual machine (default: 2048) to avoid too
much compute being used. The limit is set slightly lower on purpose to allow other necessary items in the VM.
If the contract returns more items you'll have to resort to retrieving them using RPC Session Iterators.
limit: the maximum tokens to return.
start_index: where to start collecting results from (see Note1)

Note:
This is an optional method and may not exist on the contract.
Note1:
There is a limit on the returned items in the virtual machine (default: 2048) to avoid too
much compute being used. The limit here is set slightly lower on purpose to allow other necessary items in the
VM. If the contract returns more than 2000 items you need to call the method again providing the start_index
parameter. The `tokens_count` method can be used to get the size of the iterator.

Note2:
This is an optional method in the NEP-11 standard and can thus fail.
"""

def process(res: noderpc.ExecutionResult, _: int = 0) -> list[bytes]:
raw_results: list[noderpc.StackItem] = unwrap.as_list(res)
return [si.value for si in raw_results]

sb = vm.ScriptBuilder().emit_contract_call_and_unwrap_iterator(
self.hash, "tokens", unwrap_limit=limit
self.hash, "tokens", unwrap_limit=limit, start_index=start_index
)
return ContractMethodResult(sb.to_array(), process)

Expand Down
30 changes: 23 additions & 7 deletions neo3/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,10 @@ def emit_contract_call_and_unwrap_iterator(
operation: str,
call_flags: Optional[callflags.CallFlags] = None,
unwrap_limit: int = 2000,
start_index: int = 0,
) -> ScriptBuilder:
return self._emit_contract_call_and_unwrap_iterator(
script_hash, operation, None, call_flags, unwrap_limit
script_hash, operation, None, call_flags, unwrap_limit, start_index
)

def emit_contract_call_with_args_and_count_iterator(
Expand Down Expand Up @@ -550,6 +551,7 @@ def _emit_contract_call_and_unwrap_iterator(
args: Optional[ContractParameter] = None,
call_flags: Optional[callflags.CallFlags] = None,
unwrap_limit: int = 2000,
start_index: int = 0,
) -> ScriptBuilder:
"""

Expand All @@ -563,6 +565,7 @@ def _emit_contract_call_and_unwrap_iterator(
https://github.com/neo-project/neo-vm/blob/5b0a39811b34abacab1273f3ee5a9a9f7e52ac7b/src/Neo.VM/ExecutionEngineLimits.cs#L34C21-L34C33
The current default is slightly lower than the max, because that allows for a few other items to be on the
stack in other function frames.
start_index: index in the iterator to start capturing values from
"""
# jump to local variables initialization code
self.emit_jump(OpCode.JMP, 4)
Expand All @@ -571,9 +574,9 @@ def _emit_contract_call_and_unwrap_iterator(
return_results = len(self.data)
self.emit(OpCode.LDLOC0)
self.emit(OpCode.RET)
# reserve 2 local variables for the `iterator` and `results` list
# reserve local variables for the `iterator`, the `results` list, the `start_index` and a `counter`
self.emit(OpCode.INITSLOT)
self.emit_raw(b"\x03")
self.emit_raw(b"\x04")
self.emit_raw(b"\x00")
# store results list in pos 0
self.emit(OpCode.NEWARRAY0)
Expand All @@ -588,13 +591,17 @@ def _emit_contract_call_and_unwrap_iterator(
# store stack item counter in pos 2
self.emit_push(0)
self.emit(OpCode.STLOC2)
# store the start index in pos 3
self.emit_push(start_index)
self.emit(OpCode.STLOC3)
"""
Next set of opcodes does the following

while iterator.next()
results.append(iterator.value)
if ctr >= start_index:
results.append(iterator.value)
ctr += 1
if ctr == stack_limit:
if ctr == end_index:
break
return results
"""
Expand All @@ -605,6 +612,14 @@ def _emit_contract_call_and_unwrap_iterator(
self.emit_syscall(Syscalls.SYSTEM_ITERATOR_NEXT)
# if not jump to exit routine
self.emit_jump(OpCode.JMPIFNOT, self._offset_to(return_results))
# load item counter + start_index
self.emit(OpCode.LDLOC2)
self.emit(OpCode.LDLOC3)
self.emit(OpCode.GE)
skip_append_value_to_array = (
len(self.data) + 11
) # 11 is all the instructions + operands until the 'increase counter' label
self.emit_jump(OpCode.JMPIFNOT, self._offset_to(skip_append_value_to_array))
# load iterator as argument for iterator.value
self.emit(OpCode.LDLOC1)
# get result
Expand All @@ -614,13 +629,14 @@ def _emit_contract_call_and_unwrap_iterator(
# fix argument order for APPEND
self.emit(OpCode.SWAP)
self.emit(OpCode.APPEND)
# increase counter
# load stack item counter
self.emit(OpCode.LDLOC2)
self.emit(OpCode.INC)
self.emit(OpCode.DUP)
self.emit(OpCode.STLOC2)
# load stack item limit
self.emit_push(unwrap_limit)
# load end_index
self.emit_push(start_index + unwrap_limit)
self.emit(OpCode.NUMEQUAL)
self.emit_jump(OpCode.JMPIF, self._offset_to(return_results))
# jump back to start of `while` loop
Expand Down