2021-07-12 09:18:56 -07:00
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import logging
import typing
from solana . blockhash import Blockhash
2021-10-11 09:08:54 -07:00
from solana . keypair import Keypair
2021-07-12 09:18:56 -07:00
from solana . publickey import PublicKey
from solana . transaction import Transaction , TransactionInstruction
2021-11-17 09:27:42 -08:00
from solana . utils import shortvec_encoding as shortvec
2021-07-12 09:18:56 -07:00
from . context import Context
2021-08-19 01:46:54 -07:00
from . instructionreporter import InstructionReporter
2021-07-12 09:18:56 -07:00
from . wallet import Wallet
_MAXIMUM_TRANSACTION_LENGTH = 1280 - 40 - 8
2021-11-17 09:27:42 -08:00
_PUBKEY_LENGTH = 32
2021-07-12 09:18:56 -07:00
_SIGNATURE_LENGTH = 64
2021-10-11 09:08:54 -07:00
def _split_instructions_into_chunks ( context : Context , signers : typing . Sequence [ Keypair ] , instructions : typing . Sequence [ TransactionInstruction ] ) - > typing . Sequence [ typing . Sequence [ TransactionInstruction ] ] :
2021-07-22 09:54:48 -07:00
vetted_chunks : typing . List [ typing . List [ TransactionInstruction ] ] = [ ]
current_chunk : typing . List [ TransactionInstruction ] = [ ]
2022-01-22 08:51:04 -08:00
for counter , instruction in enumerate ( instructions ) :
2021-07-22 09:54:48 -07:00
instruction_size_on_its_own = CombinableInstructions . transaction_size ( signers , [ instruction ] )
if instruction_size_on_its_own > = _MAXIMUM_TRANSACTION_LENGTH :
2021-09-09 11:02:17 -07:00
report = context . client . instruction_reporter . report ( instruction )
2021-07-22 09:54:48 -07:00
raise Exception (
2022-01-22 08:51:04 -08:00
f " Instruction exceeds maximum size - instruction { counter } has { len ( instruction . keys ) } keys and creates a transaction { instruction_size_on_its_own } bytes long: \n { report } " )
2021-07-22 09:54:48 -07:00
in_progress_chunk = current_chunk + [ instruction ]
transaction_size = CombinableInstructions . transaction_size ( signers , in_progress_chunk )
if transaction_size < _MAXIMUM_TRANSACTION_LENGTH :
current_chunk = in_progress_chunk
else :
vetted_chunks + = [ current_chunk ]
current_chunk = [ instruction ]
all_chunks = vetted_chunks + [ current_chunk ]
total_in_chunks = sum ( map ( lambda chunk : len ( chunk ) , all_chunks ) )
if total_in_chunks != len ( instructions ) :
raise Exception (
f " Failed to chunk instructions. Have { total_in_chunks } instuctions in chunks. Should have { len ( instructions ) } . " )
return all_chunks
2021-07-12 09:18:56 -07:00
# 🥭 CombinableInstructions class
#
# This class wraps up zero or more Solana instructions and signers, and allows instances to be combined
# easily into a single instance. This instance can then be executed.
#
# This allows simple uses like, for example:
# ```
# (signers + place_orders + settle + crank).execute(context)
# ```
#
class CombinableInstructions ( ) :
2021-11-23 03:25:54 -08:00
# A toggle to run both checks to ensure our calculations are accurate.
__check_transaction_size_with_pyserum = False
2021-11-17 09:27:42 -08:00
2021-11-09 05:23:36 -08:00
def __init__ ( self , signers : typing . Sequence [ Keypair ] , instructions : typing . Sequence [ TransactionInstruction ] ) - > None :
2021-12-13 03:15:24 -08:00
self . _logger : logging . Logger = logging . getLogger ( self . __class__ . __name__ )
2021-10-11 09:08:54 -07:00
self . signers : typing . Sequence [ Keypair ] = signers
2021-07-12 09:18:56 -07:00
self . instructions : typing . Sequence [ TransactionInstruction ] = instructions
@staticmethod
def empty ( ) - > " CombinableInstructions " :
return CombinableInstructions ( signers = [ ] , instructions = [ ] )
@staticmethod
2021-10-11 09:08:54 -07:00
def from_signers ( signers : typing . Sequence [ Keypair ] ) - > " CombinableInstructions " :
2021-07-12 09:18:56 -07:00
return CombinableInstructions ( signers = signers , instructions = [ ] )
@staticmethod
def from_wallet ( wallet : Wallet ) - > " CombinableInstructions " :
2021-10-11 09:08:54 -07:00
return CombinableInstructions ( signers = [ wallet . keypair ] , instructions = [ ] )
2021-07-12 09:18:56 -07:00
@staticmethod
def from_instruction ( instruction : TransactionInstruction ) - > " CombinableInstructions " :
return CombinableInstructions ( signers = [ ] , instructions = [ instruction ] )
2021-11-17 09:27:42 -08:00
# This is the expensive - but always accurate - way of calculating the size of a transaction.
2021-07-12 09:18:56 -07:00
@staticmethod
2021-11-17 09:27:42 -08:00
def _transaction_size_from_pyserum ( signers : typing . Sequence [ Keypair ] , instructions : typing . Sequence [ TransactionInstruction ] ) - > int :
2021-07-12 09:18:56 -07:00
inspector = Transaction ( )
inspector . recent_blockhash = Blockhash ( str ( PublicKey ( 3 ) ) )
inspector . instructions . extend ( instructions )
inspector . sign ( * signers )
signed_data = inspector . serialize_message ( )
length : int = len ( signed_data )
# Signature count length
length + = 1
# Signatures
length + = ( len ( inspector . signatures ) * _SIGNATURE_LENGTH )
return length
2021-11-17 09:27:42 -08:00
# This is the quicker way - just add up the sizes ourselves. It's not trivial though.
@staticmethod
def _calculate_transaction_size ( signers : typing . Sequence [ Keypair ] , instructions : typing . Sequence [ TransactionInstruction ] ) - > int :
# Solana transactions have a deterministic size, but calculating it is a bit tricky.
#
# The transaction consists of:
# * Message header
# * count of number of instruction parcels
# * 1 instruction parcel per instruction
#
# We're mostly interested in the number of distinct public keys used, rather than just the total number of public keys used
# All distinct keys are passed in the message header, and 1-byte indexes are used in the instructions. That makes it all
# a bit more compact.
#
# `shortvec` here is a compact representation of an integer that may take multiple bytes but only as many as it needs. What
# we're interest in here is the count of the number of bytes the `shortvec` takes. It's normally 1, sometimes 2, but could be
# any number.
#
# A public key is 32 bytes.
#
# A signature is 64 bytes.
#
# Message header is:
# * 1 byte for number of signatures
# * 1 byte for number of signed accounts
# * 1 byte for number of unsigned accounts
# * 32 bytes for recent blockhash
# * (variable) shortvec length for number of distinct public keys
# * (variable) 32 bytes * number of distinct public keys
#
# This gives us a header size of:
# 35 + (shortvec-length of distinct public keys) + (32 * number of distinct public keys)
#
# The count of number of instruction parcels is a shortvec length of the number of instructions
#
# Each instruction is:
# * 1 byte for the program ID index
# * (variable) shortvec length of the number of public keys
# * (variable) 1 byte for the index of each account public key
# * (variable) shortvec length of the data
# * (variable) length of the data
#
# This gives us an instruction size of:
# 1 + (shortvec-length of number of keys) + (number of keys) + (shortvec-length of the data) + (length of the data)
#
# This is signed, so the length is then:
# * Message header size
# * + each instruction size
# * + shortvec-length of the number of signers
# * + (number of signers * 64 bytes)
#
def shortvec_length ( value : int ) - > int :
return len ( shortvec . encode_length ( value ) )
program_ids = { instruction . program_id . to_base58 ( ) for instruction in instructions }
meta_pubkeys = { meta . pubkey . to_base58 ( ) for instruction in instructions for meta in instruction . keys }
distinct_publickeys = set . union ( program_ids , meta_pubkeys , {
signer . public_key . to_base58 ( ) for signer in signers } )
num_distinct_publickeys = len ( distinct_publickeys )
# 35 + (shortvec-length of distinct public keys) + (32 * number of distinct public keys)
header_size = 35 + shortvec_length ( num_distinct_publickeys ) + ( num_distinct_publickeys * _PUBKEY_LENGTH )
instruction_count_length = shortvec_length ( len ( instructions ) )
instructions_size = 0
for inst in instructions :
# 1 + (shortvec-length of number of keys) + (number of keys) + (shortvec-length of the data) + (length of the data)
instructions_size + = 1 + shortvec_length ( len ( inst . keys ) ) + len ( inst . keys ) + \
shortvec_length ( len ( inst . data ) ) + len ( inst . data )
# Signatures
signatures_size = 1 + ( len ( signers ) * _SIGNATURE_LENGTH )
# We can now calculate the total transaction size
calculated_transaction_size = header_size + instruction_count_length + instructions_size + signatures_size
return calculated_transaction_size
# Calculate the exact size of a transaction. There's an upper limit of 1232 so we need to keep
# all transactions below this size.
@staticmethod
def transaction_size ( signers : typing . Sequence [ Keypair ] , instructions : typing . Sequence [ TransactionInstruction ] ) - > int :
calculated_transaction_size = CombinableInstructions . _calculate_transaction_size ( signers , instructions )
if CombinableInstructions . __check_transaction_size_with_pyserum :
2021-11-22 09:09:39 -08:00
pyserum_transaction_size = CombinableInstructions . _transaction_size_from_pyserum ( signers , instructions )
2021-11-17 09:27:42 -08:00
discrepancy = pyserum_transaction_size - calculated_transaction_size
if discrepancy == 0 :
logging . debug (
f " txszcalc Calculated: { calculated_transaction_size } , Should be: { pyserum_transaction_size } , No Discrepancy! " )
else :
logging . error (
f " txszcalcerr Calculated: { calculated_transaction_size } , Should be: { pyserum_transaction_size } , Discrepancy: { discrepancy } " )
return pyserum_transaction_size
return calculated_transaction_size
2021-07-12 09:18:56 -07:00
def __add__ ( self , new_instruction_data : " CombinableInstructions " ) - > " CombinableInstructions " :
all_signers = [ * self . signers , * new_instruction_data . signers ]
all_instructions = [ * self . instructions , * new_instruction_data . instructions ]
return CombinableInstructions ( signers = all_signers , instructions = all_instructions )
2021-08-07 07:07:19 -07:00
def execute ( self , context : Context , on_exception_continue : bool = False ) - > typing . Sequence [ str ] :
2021-07-22 09:54:48 -07:00
chunks : typing . Sequence [ typing . Sequence [ TransactionInstruction ]
2021-09-09 11:02:17 -07:00
] = _split_instructions_into_chunks ( context , self . signers , self . instructions )
2021-07-22 09:54:48 -07:00
if len ( chunks ) == 1 and len ( chunks [ 0 ] ) == 0 :
2021-12-13 03:15:24 -08:00
self . _logger . info ( " No instructions to run. " )
2021-07-15 13:03:22 -07:00
return [ ]
2021-07-22 09:54:48 -07:00
if len ( chunks ) > 1 :
2021-12-13 03:15:24 -08:00
self . _logger . info ( f " Running instructions in { len ( chunks ) } transactions. " )
2021-07-15 13:03:22 -07:00
2021-08-07 07:07:19 -07:00
results : typing . List [ str ] = [ ]
2021-07-22 09:54:48 -07:00
for index , chunk in enumerate ( chunks ) :
transaction = Transaction ( )
transaction . instructions . extend ( chunk )
try :
2021-08-07 07:07:19 -07:00
response = context . client . send_transaction ( transaction , * self . signers )
results + = [ response ]
2021-08-04 09:50:38 -07:00
except Exception as exception :
2021-07-22 09:54:48 -07:00
starts_at = sum ( len ( ch ) for ch in chunks [ 0 : index ] )
2021-08-19 01:46:54 -07:00
if on_exception_continue :
2021-12-13 03:15:24 -08:00
self . _logger . error ( f """ [ { context . name } ] Error executing chunk { index } (instructions { starts_at } to { starts_at + len ( chunk ) } ) of CombinableInstruction.
2021-08-19 01:46:54 -07:00
{ exception } """ )
else :
2021-08-07 07:07:19 -07:00
raise exception
2021-07-22 09:54:48 -07:00
return results
2021-12-01 11:22:36 -08:00
async def execute_async ( self , context : Context , on_exception_continue : bool = False ) - > typing . Sequence [ str ] :
return self . execute ( context , on_exception_continue )
2021-07-12 09:18:56 -07:00
def __str__ ( self ) - > str :
report : typing . List [ str ] = [ ]
for index , signer in enumerate ( self . signers ) :
2021-10-11 09:08:54 -07:00
report + = [ f " Signer[ { index } ]: { signer . public_key } " ]
2021-07-12 09:18:56 -07:00
2021-08-19 01:46:54 -07:00
instruction_reporter : InstructionReporter = InstructionReporter ( )
2021-08-03 01:14:22 -07:00
for instruction in self . instructions :
2021-08-19 01:46:54 -07:00
report + = [ instruction_reporter . report ( instruction ) ]
2021-07-12 09:18:56 -07:00
return " \n " . join ( report )
def __repr__ ( self ) - > str :
return f " { self } "