sbv2-solana/libraries/py/switchboardpy/crank.py

380 lines
13 KiB
Python

import anchorpy
import math
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional
from solana import system_program
from solana.keypair import Keypair
from solana.publickey import PublicKey
from spl.token.constants import TOKEN_PROGRAM_ID
from spl.token.instructions import get_associated_token_address
from switchboardpy.lease import LeaseAccount
from switchboardpy.permission import PermissionAccount
from switchboardpy.common import AccountParams
from switchboardpy.oraclequeue import OracleQueueAccount
from switchboardpy.aggregator import AggregatorAccount
from solana.system_program import CreateAccountParams, create_account
from switchboardpy.program import ProgramStateAccount
from .generated.accounts import CrankAccountData
# Parameters for initializing a CrankAccount
@dataclass
class CrankInitParams:
"""OracleQueueAccount for which this crank is associated"""
queue_account: OracleQueueAccount
"""Buffer specifying crank name"""
name: bytes = None
"""Buffer specifying crank metadata"""
metadata: bytes = None
"""Optional max number of rows"""
max_rows: int = None
# Parameters for popping an element from a CrankAccount
@dataclass
class CrankPopParams:
"""Specifies the wallet to reward for turning the crank."""
payout_wallet: PublicKey
"""The pubkey of the linked oracle queue."""
queue_pubkey: PublicKey
"""The pubkey of the linked oracle queue authority."""
queue_authority: PublicKey
"""CrankAccount data"""
crank: Any
"""QueueAccount data"""
queue: Any
"""Token mint pubkey"""
token_mint: PublicKey
"""
Array of pubkeys to attempt to pop. If discluded, this will be loaded
from the crank upon calling.
"""
ready_pubkeys: list[PublicKey] = None
"""Nonce to allow consecutive crank pops with the same blockhash."""
nonce: int = None
fail_open_on_mismatch: bool = None
# Parameters for pushing an element into a CrankAccount
@dataclass
class CrankPushParams:
aggregator_account: AggregatorAccount
# Row structure of elements in the crank
@dataclass
class CrankRow:
"""Aggregator account pubkey"""
pubkey: PublicKey
"""Next aggregator update timestamp to order the crank by"""
next_timestamp: int
@staticmethod
def from_bytes(buf: bytes):
pass
class CrankAccount:
""" A Switchboard account representing a crank of aggregators ordered by next update time.
Attributes:
program (anchor.Program): The anchor program ref
public_key (PublicKey | None): This crank's public key
keypair (Keypair | None): this crank's keypair
"""
def __init__(self, params: AccountParams):
if params.public_key is None and params.keypair is None:
raise ValueError('User must provide either a publicKey or keypair for account use.')
if params.keypair and params.public_key and params.keypair.public_key != params.public_key:
raise ValueError('User must provide either a publicKey or keypair for account use.')
self.program = params.program
self.public_key = params.keypair.public_key if params.keypair else params.public_key
self.keypair = params.keypair
"""
Get the size of an CrankAccount on chain
Args:
Returns:
int: size of the CrankAccount type on chain
"""
def size(self):
return self.program.account["CrankAccountData"].size
"""
Load and parse CrankAccount data based on the program IDL
Args:
Returns:
CrankAccount
Raises:
AccountDoesNotExistError: If the account doesn't exist.
AccountInvalidDiscriminator: If the discriminator doesn't match the IDL.
"""
async def load_data(self):
return await CrankAccountData.fetch(self.program.provider.connection, self.public_key)
"""
Create and initialize the CrankAccount.
Args:
program (anchor.Program): Switchboard program representation holding connection and IDL.
params (CrankInitParams)
Returns:
CrankAccount
"""
@staticmethod
async def create(program: anchorpy.Program, params: CrankInitParams):
crank_account = Keypair.generate()
buffer = Keypair.generate()
size = program.account["CrankAccountData"].size
max_rows = params.max_rows or 500
crank_size = max_rows * 40 + 8
response = await program.provider.connection.get_minimum_balance_for_rent_exemption(crank_size)
lamports = response["result"]
await program.rpc["crank_init"](
{
"name": params.name or bytes([0] * 32),
"metadata": params.metadata or bytes([0] * 128),
"crank_size": max_rows
},
ctx=anchorpy.Context(
accounts={
"crank": crank_account.public_key,
"queue": params.queue_account.public_key,
"buffer": buffer.public_key,
"system_program": system_program.SYS_PROGRAM_ID,
"payer": program.provider.wallet.public_key
},
signers=[crank_account, buffer],
pre_instructions=[
create_account(
CreateAccountParams(
from_pubkey=program.provider.wallet.public_key,
new_account_pubkey=buffer.public_key,
lamports=lamports,
space=size,
program_id=program.program_id
)
)
]
)
)
return CrankAccount(AccountParams(program=program, keypair=crank_account))
"""
Pushes a new aggregator onto the crank
Args:
params (CrankPushParams): aggregator and related data
Returns:
TransactionSignature
"""
async def push(self, params: CrankPushParams):
aggregator_account: AggregatorAccount = params.aggregator_account
crank = await self.load_data()
queue_account = OracleQueueAccount(AccountParams(program=self.program, public_key=crank.queue_pubkey))
queue = await queue_account.load_data()
queue_authority = queue.authority
lease_account, lease_bump = LeaseAccount.from_seed(self.program, queue_account, aggregator_account)
lease: Any = None
try:
lease = await lease_account.load_data()
except Exception:
raise ValueError('A requested lease pda account has not been initialized.')
permission_account, permission_bump = PermissionAccount.from_seed(
self.program,
queue_authority,
queue_account.public_key,
aggregator_account.public_key
)
try:
await lease_account.load_data()
except Exception:
raise ValueError('A requested permission pda account has not been initialized.')
program_state_account, state_bump = ProgramStateAccount.from_seed(self.program)
return await self.program.rpc["crank_push"](
{
"state_bump": state_bump,
"permission_bump": permission_bump
},
ctx=anchorpy.Context(
accounts={
"crank": self.public_key,
"aggregator": aggregator_account.public_key,
"oracle_queue": queue_account.public_key,
"queue_authority": queue_authority,
"permission": permission_account.public_key,
"lease": lease_account.public_key,
"escrow": lease.escrow,
"program_state": program_state_account.public_key,
"data_buffer": crank.data_buffer
}
)
)
"""
Pops a tx from the crank.
Args:
params (CrankPopParams)
Returns:
TransactionSignature
"""
async def pop_txn(self, params: CrankPopParams):
fail_open_on_account_mismatch = params.fail_open_on_mismatch or False
next = params.ready_pubkeys or await self.peak_next_ready(5)
if len(next) == 0:
raise ValueError('Crank is not ready to be turned')
remaining_accounts: list[PublicKey] = []
lease_bumps_map: Dict[str, int] = {}
permission_bumps_map: Dict[str, int] = {}
queue_account = OracleQueueAccount(AccountParams(program=self.program, public_key=params.queue_pubkey))
for row in next:
aggregator_account = AggregatorAccount(AccountParams(program=self.program, public_key=row))
lease_account, lease_bump = LeaseAccount.from_seed(
self.program,
queue_account,
aggregator_account
)
permission_account, permission_bump = PermissionAccount.from_seed(
self.program,
params.queue_authority,
params.queue_pubkey,
row
)
escrow = get_associated_token_address(
lease_account.public_key,
params.token_mint
)
remaining_accounts.append(aggregator_account.public_key)
remaining_accounts.append(lease_account.public_key)
remaining_accounts.append(escrow)
remaining_accounts.append(permission_account.public_key)
lease_bumps_map[row.to_base58()] = lease_bump
permission_bumps_map[row.to_base58()] = permission_bump
remaining_accounts.sort(key=lambda key : bytes(key))
crank = params.crank
queue = params.queue
lease_bumps: list[int] = []
permission_bumps: list[int] = []
for key in remaining_accounts:
lease_bumps.append(lease_bumps_map.get(key.to_base58()) or 0)
permission_bumps.append(permission_bumps_map.get(key.to_base58()) or 0)
program_state_account, state_bump = ProgramStateAccount.from_seed(self.program)
payer_keypair = Keypair.from_secret_key(self.program.provider.wallet.payer.secret_key)
return self.program.transaction["crank_pop"](
{
"state_bump": state_bump,
"lease_bumps": bytes(lease_bumps),
"permission_bumps": bytes(permission_bumps),
"nonce": params.nonce or None,
"fail_open_on_account_mismatch": fail_open_on_account_mismatch
},
ctx=anchorpy.Context(
accounts={
"crank": self.public_key,
"oracle_queue": params.queue_pubkey,
"queue_authority": params.queue_authority,
"program_state": program_state_account.public_key,
"payout_wallet": params.payout_wallet,
"token_program": TOKEN_PROGRAM_ID,
"crank_data_buffer": crank.data_buffer,
"queue_data_buffer": queue.data_buffer
},
remaining_accounts=[{ "is_signer": False, "is_writable": True, "pubkey": pubkey } for pubkey in remaining_accounts],
signers=[payer_keypair]
)
)
"""
Pops an aggregator from the crank
Args:
params (CrankPopParams)
Returns:
TransactionSignature
"""
async def pop(self, params: CrankPopParams):
payer_keypair = Keypair.from_secret_key(self.program.provider.wallet.payer.secret_key)
txn = await self.pop_txn(params)
return await self.program.provider.connection.send_transaction(txn, [payer_keypair])
"""
Get an array of the next aggregator pubkeys to be popped from the crank, limited by n
Args:
n (int): limit of pubkeys to return
Returns:
list[CrankRow]: Pubkey list of Aggregators and next timestamp to be popped, ordered by timestamp
"""
async def peak_next_with_time(self, n: int):
crank = await self.load_data()
# get list slice of length pq_size
pq_data: list[CrankRow] = crank.pq_data[:crank.pq_size]
# sort by CrankRow next timestamp
pq_data.sort(key=lambda crank_row: crank_row.next_timestamp)
# return items
return pq_data[:n]
"""
Get an array of the next readily updateable aggregator pubkeys to be popped
from the crank, limited by n
Args:
n (Optional[int]): limit of pubkeys to return
Returns:
list[PublicKey]: Pubkey list of Aggregators and next timestamp to be popped, ordered by timestamp
"""
async def peak_next_ready(self, n: Optional[int] = None):
now = math.floor(time.time())
crank = await self.load_data()
pq_data: list[CrankRow] = crank.pq_data[:crank.pq_size]
key = lambda crank_row: crank_row.next_timestamp
return [item.pubkey for item in list(filter(lambda item: now >= item.next_timestamp, pq_data)).sort(key=key)[:(n or len(pq_data))]]
"""
Get an array of the next aggregator pubkeys to be popped from the crank, limited by n
Args:
n (int): limit of pubkeys to return
Returns:
list[PublicKey]: Pubkey list of Aggregators and next timestamp to be popped, ordered by timestamp
"""
async def peak_next(self, n: int):
crank = await self.load_data()
pq_data: list[CrankRow] = crank.pq_data[:crank.pq_size]
pq_data.sort(key=lambda crank_row: crank_row.next_timestamp)
return [item.pubkey for item in pq_data[:n]]