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
|
|
|
|
|
|
|
|
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
|
|
|
|
_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] = []
|
|
|
|
for instruction in instructions:
|
|
|
|
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(
|
2021-09-09 11:02:17 -07:00
|
|
|
f"Instruction exceeds maximum size - 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-09 05:23:36 -08:00
|
|
|
def __init__(self, signers: typing.Sequence[Keypair], instructions: typing.Sequence[TransactionInstruction]) -> None:
|
2021-07-12 09:18:56 -07: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])
|
|
|
|
|
|
|
|
# This is a quick-and-dirty way to find out the size the transaction will be. There's an upper limit
|
|
|
|
# on transaction size of 1232 so we need to keep all transactions below this size.
|
|
|
|
@staticmethod
|
2021-10-11 09:08:54 -07:00
|
|
|
def transaction_size(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
|
|
|
|
|
|
|
|
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-07-15 13:03:22 -07:00
|
|
|
self.logger.info("No instructions to run.")
|
|
|
|
return []
|
|
|
|
|
2021-07-22 09:54:48 -07:00
|
|
|
if len(chunks) > 1:
|
|
|
|
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:
|
|
|
|
self.logger.error(f"""[{context.name}] Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction.
|
|
|
|
{exception}""")
|
|
|
|
else:
|
2021-08-07 07:07:19 -07:00
|
|
|
raise exception
|
2021-07-22 09:54:48 -07:00
|
|
|
|
|
|
|
return results
|
|
|
|
|
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}"
|