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
2022-03-03 06:25:40 -08:00
from decimal import Decimal
2021-07-12 09:18:56 -07:00
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
2022-03-03 06:25:40 -08:00
from mango . constants import SOL_DECIMAL_DIVISOR
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
2022-02-09 11:31:50 -08: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 ) :
2022-02-09 11:31:50 -08:00
instruction_size_on_its_own = CombinableInstructions . transaction_size (
signers , [ instruction ]
)
2021-07-22 09:54:48 -07:00
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-02-09 11:31:50 -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 ]
2022-02-09 11:31:50 -08:00
transaction_size = CombinableInstructions . transaction_size (
signers , in_progress_chunk
)
2021-07-22 09:54:48 -07:00
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 (
2022-02-09 11:31:50 -08:00
f " Failed to chunk instructions. Have { total_in_chunks } instuctions in chunks. Should have { len ( instructions ) } . "
)
2021-07-22 09:54:48 -07:00
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)
# ```
#
2022-02-09 11:31:50 -08:00
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
2022-02-09 11:31:50 -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
2022-02-09 11:31:50 -08:00
def from_instruction (
instruction : TransactionInstruction ,
) - > " CombinableInstructions " :
2021-07-12 09:18:56 -07:00
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
2022-02-09 11:31:50 -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
2022-02-09 11:31:50 -08:00
length + = len ( inspector . signatures ) * _SIGNATURE_LENGTH
2021-07-12 09:18:56 -07:00
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
2022-02-09 11:31:50 -08:00
def _calculate_transaction_size (
signers : typing . Sequence [ Keypair ] ,
instructions : typing . Sequence [ TransactionInstruction ] ,
) - > int :
2021-11-17 09:27:42 -08:00
# 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 ) )
2022-02-09 11:31:50 -08:00
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 } ,
)
2021-11-17 09:27:42 -08:00
num_distinct_publickeys = len ( distinct_publickeys )
# 35 + (shortvec-length of distinct public keys) + (32 * number of distinct public keys)
2022-02-09 11:31:50 -08:00
header_size = (
35
+ shortvec_length ( num_distinct_publickeys )
+ ( num_distinct_publickeys * _PUBKEY_LENGTH )
)
2021-11-17 09:27:42 -08:00
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)
2022-02-09 11:31:50 -08:00
instructions_size + = (
1
+ shortvec_length ( len ( inst . keys ) )
+ len ( inst . keys )
+ shortvec_length ( len ( inst . data ) )
+ len ( inst . data )
)
2021-11-17 09:27:42 -08:00
# Signatures
signatures_size = 1 + ( len ( signers ) * _SIGNATURE_LENGTH )
# We can now calculate the total transaction size
2022-02-09 11:31:50 -08:00
calculated_transaction_size = (
header_size + instruction_count_length + instructions_size + signatures_size
)
2021-11-17 09:27:42 -08:00
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
2022-02-09 11:31:50 -08:00
def transaction_size (
signers : typing . Sequence [ Keypair ] ,
instructions : typing . Sequence [ TransactionInstruction ] ,
) - > int :
calculated_transaction_size = (
CombinableInstructions . _calculate_transaction_size ( signers , instructions )
)
2021-11-17 09:27:42 -08:00
if CombinableInstructions . __check_transaction_size_with_pyserum :
2022-02-09 11:31:50 -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 (
2022-02-09 11:31:50 -08:00
f " txszcalc Calculated: { calculated_transaction_size } , Should be: { pyserum_transaction_size } , No Discrepancy! "
)
2021-11-17 09:27:42 -08:00
else :
logging . error (
2022-02-09 11:31:50 -08:00
f " txszcalcerr Calculated: { calculated_transaction_size } , Should be: { pyserum_transaction_size } , Discrepancy: { discrepancy } "
)
2021-11-17 09:27:42 -08:00
return pyserum_transaction_size
return calculated_transaction_size
2022-02-09 11:31:50 -08:00
def __add__ (
self , new_instruction_data : " CombinableInstructions "
) - > " CombinableInstructions " :
2021-07-12 09:18:56 -07:00
all_signers = [ * self . signers , * new_instruction_data . signers ]
all_instructions = [ * self . instructions , * new_instruction_data . instructions ]
2022-02-09 11:31:50 -08:00
return CombinableInstructions (
signers = all_signers , instructions = all_instructions
)
2021-07-12 09:18:56 -07:00
2022-03-02 10:46:33 -08:00
@property
def is_empty ( self ) - > bool :
return len ( self . signers ) == 0 and len ( self . instructions ) == 0
2022-02-09 11:31:50 -08:00
def execute (
self , context : Context , on_exception_continue : bool = False
) - > typing . Sequence [ str ] :
chunks : typing . Sequence [
typing . Sequence [ TransactionInstruction ]
] = _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 :
2022-02-09 11:31:50 -08:00
self . _logger . error (
f """ [ { context . name } ] Error executing chunk { index } (instructions { starts_at } to { starts_at + len ( chunk ) } ) of CombinableInstruction.
{ exception } """
)
2021-08-19 01:46:54 -07:00
else :
2021-08-07 07:07:19 -07:00
raise exception
2021-07-22 09:54:48 -07:00
return results
2022-02-09 11:31:50 -08:00
async def execute_async (
self , context : Context , on_exception_continue : bool = False
) - > typing . Sequence [ str ] :
2021-12-01 11:22:36 -08:00
return self . execute ( context , on_exception_continue )
2022-03-03 06:25:40 -08:00
def cost_to_execute ( self , context : Context ) - > Decimal :
# getFees() is depracated and will be replaced by getFeeForMessage() at some point.
# getFeeForMessage() is not fully available yet though.
fee_response = context . client . compatible_client . get_fees ( )
# fee_response should look like:
# {
# "jsonrpc": "2.0",
# "result": {
# "context": {
# "slot": 1
# },
# "value": {
# "blockhash": "CSymwgTNX1j3E4qhKfJAUE41nBWEwXufoYryPbkde5RR",
# "feeCalculator": {
# "lamportsPerSignature": 5000
# },
# "lastValidSlot": 297,
# "lastValidBlockHeight": 296
# }
# },
# "id": 1
# }
number_of_signatures = len ( self . signers )
lamports_per_signature = fee_response [ " result " ] [ " value " ] [ " feeCalculator " ] [
" lamportsPerSignature "
]
fee_in_lamports = Decimal ( number_of_signatures ) * Decimal (
lamports_per_signature
)
return fee_in_lamports / SOL_DECIMAL_DIVISOR
2022-03-08 00:53:11 -08:00
def report ( self , instruction_reporter : InstructionReporter ) - > str :
2021-07-12 09:18:56 -07:00
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-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 )
2022-03-08 00:53:11 -08:00
def __str__ ( self ) - > str :
return self . report ( InstructionReporter ( ) )
2021-07-12 09:18:56 -07:00
def __repr__ ( self ) - > str :
return f " { self } "