517 lines
22 KiB
Python
517 lines
22 KiB
Python
import json
|
|
from http import HTTPStatus
|
|
from cryptography.fernet import Fernet
|
|
import base64
|
|
import base58
|
|
import struct
|
|
|
|
from solana.publickey import PublicKey
|
|
from solana.transaction import Transaction, AccountMeta, TransactionInstruction
|
|
from solana.account import Account
|
|
from solana.rpc.api import Client
|
|
import solana.rpc.types as types
|
|
from solana.system_program import transfer, TransferParams
|
|
from spl.token._layouts import MINT_LAYOUT, ACCOUNT_LAYOUT
|
|
from spl.token.instructions import (
|
|
get_associated_token_address, create_associated_token_account,
|
|
mint_to, MintToParams,
|
|
)
|
|
|
|
SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'
|
|
SYSVAR_RENT_ID = 'SysvarRent111111111111111111111111111111111'
|
|
ASSOCIATED_TOKEN_ACCOUNT_PROGRAM_ID = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
|
|
TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
BINARY_OPTION_PROGRAM_ID = 'betw959P4WToez4DkuXwNsJszqbpe3HuY56AcG5yevx'
|
|
|
|
|
|
def initialize_binary_option_instruction(
|
|
pool_account,
|
|
escrow_mint_account,
|
|
escrow_account,
|
|
long_token_mint_account,
|
|
short_token_mint_account,
|
|
mint_authority_account,
|
|
update_authority_account,
|
|
token_account,
|
|
system_account,
|
|
rent_account,
|
|
decimals
|
|
):
|
|
keys = [
|
|
AccountMeta(pubkey=pool_account, is_signer=True, is_writable=True),
|
|
AccountMeta(pubkey=escrow_mint_account, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=escrow_account, is_signer=True, is_writable=True),
|
|
AccountMeta(pubkey=long_token_mint_account, is_signer=True, is_writable=False),
|
|
AccountMeta(pubkey=short_token_mint_account, is_signer=True, is_writable=False),
|
|
AccountMeta(pubkey=mint_authority_account, is_signer=True, is_writable=False),
|
|
AccountMeta(pubkey=update_authority_account, is_signer=True, is_writable=False),
|
|
AccountMeta(pubkey=token_account, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=system_account, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=rent_account, is_signer=False, is_writable=False),
|
|
]
|
|
data = struct.pack("<BB", 0, decimals)
|
|
return TransactionInstruction(keys=keys, program_id=PublicKey(BINARY_OPTION_PROGRAM_ID), data=data)
|
|
|
|
def trade_instruction(
|
|
pool_account,
|
|
escrow_account,
|
|
long_token_mint_account,
|
|
short_token_mint_account,
|
|
buyer,
|
|
seller,
|
|
buyer_account,
|
|
seller_account,
|
|
buyer_long_token_account,
|
|
buyer_short_token_account,
|
|
seller_long_token_account,
|
|
seller_short_token_account,
|
|
escrow_authority_account,
|
|
token_account,
|
|
size,
|
|
buyer_price,
|
|
seller_price,
|
|
):
|
|
keys = [
|
|
AccountMeta(pubkey=pool_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=escrow_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=long_token_mint_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=short_token_mint_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=buyer, is_signer=True, is_writable=False),
|
|
AccountMeta(pubkey=seller, is_signer=True, is_writable=False),
|
|
AccountMeta(pubkey=buyer_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=seller_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=buyer_long_token_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=buyer_short_token_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=seller_long_token_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=seller_short_token_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=escrow_authority_account, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=token_account, is_signer=False, is_writable=False),
|
|
]
|
|
data = struct.pack("<BQQQ", 1, size, buyer_price, seller_price)
|
|
return TransactionInstruction(keys=keys, program_id=PublicKey(BINARY_OPTION_PROGRAM_ID), data=data)
|
|
|
|
def settle_instruction(
|
|
pool_account,
|
|
winning_mint_account,
|
|
pool_owner_account,
|
|
):
|
|
keys = [
|
|
AccountMeta(pubkey=pool_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=winning_mint_account, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=pool_owner_account, is_signer=True, is_writable=False),
|
|
]
|
|
data = struct.pack("<B", 2)
|
|
return TransactionInstruction(keys=keys, program_id=PublicKey(BINARY_OPTION_PROGRAM_ID), data=data)
|
|
|
|
def collect_instruction(
|
|
pool_account,
|
|
collector_account,
|
|
collector_long_token_account,
|
|
collector_short_token_account,
|
|
collector_collateral_account,
|
|
long_token_mint_account,
|
|
short_token_mint_account,
|
|
escrow_account,
|
|
escrow_authority_account,
|
|
token_account,
|
|
):
|
|
keys = [
|
|
AccountMeta(pubkey=pool_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=collector_account, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=collector_long_token_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=collector_short_token_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=collector_collateral_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=long_token_mint_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=short_token_mint_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=escrow_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=escrow_authority_account, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=token_account, is_signer=False, is_writable=False),
|
|
]
|
|
data = struct.pack("<B", 3)
|
|
return TransactionInstruction(keys=keys, program_id=PublicKey(BINARY_OPTION_PROGRAM_ID), data=data)
|
|
|
|
class BinaryOption():
|
|
|
|
def __init__(self, cfg):
|
|
self.private_key = list(base58.b58decode(cfg["PRIVATE_KEY"]))[:32]
|
|
self.public_key = cfg["PUBLIC_KEY"]
|
|
self.cipher = Fernet(cfg["DECRYPTION_KEY"])
|
|
|
|
|
|
def initialize(self, api_endpoint, escrow_mint, decimals=2, skip_confirmation=True):
|
|
msg = ""
|
|
# Initialize Clinet
|
|
client = Client(api_endpoint)
|
|
msg += "Initialized client"
|
|
# Create account objects
|
|
source_account = Account(self.private_key)
|
|
pool = Account()
|
|
long_escrow = Account()
|
|
short_escrow = Account()
|
|
long_mint = Account()
|
|
short_mint = Account()
|
|
# List non-derived accounts
|
|
pool_account = pool.public_key()
|
|
escrow_mint_account = PublicKey(escrow_mint)
|
|
escrow_account = long_escrow.public_key()
|
|
long_token_mint_account = long_mint.public_key()
|
|
short_token_mint_account = short_mint.public_key()
|
|
mint_authority_account = source_account.public_key()
|
|
update_authority_account = source_account.public_key()
|
|
token_account = PublicKey(TOKEN_PROGRAM_ID)
|
|
system_account = PublicKey(SYSTEM_PROGRAM_ID)
|
|
rent_account = PublicKey(SYSVAR_RENT_ID)
|
|
msg += " | Gathered accounts"
|
|
# List signers
|
|
signers = [source_account, long_mint, short_mint, long_escrow, short_escrow, pool]
|
|
# Start transaction
|
|
tx = Transaction()
|
|
# Create Token Metadata
|
|
init_binary_option_ix = initialize_binary_option_instruction(
|
|
pool_account,
|
|
escrow_mint_account,
|
|
escrow_account,
|
|
long_token_mint_account,
|
|
short_token_mint_account,
|
|
mint_authority_account,
|
|
update_authority_account,
|
|
token_account,
|
|
system_account,
|
|
rent_account,
|
|
decimals,
|
|
)
|
|
tx = tx.add(init_binary_option_ix)
|
|
msg += f" | Creating binary option"
|
|
# Send request
|
|
try:
|
|
response = client.send_transaction(tx, *signers, opts=types.TxOpts(skip_confirmation=skip_confirmation))
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.OK,
|
|
'binary_option': str(pool_account),
|
|
'msg': msg + f" | Successfully created binary option {str(pool_account)}",
|
|
'tx': response.get('result') if skip_confirmation else response['result']['transaction']['signatures'],
|
|
}
|
|
)
|
|
except Exception as e:
|
|
msg += f" | ERROR: Encountered exception while attempting to send transaction: {e}"
|
|
raise(e)
|
|
|
|
|
|
def trade(self, api_endpoint, pool_account, buyer_encrypted_private_key, seller_encrypted_private_key, size, buyer_price, seller_price, skip_confirmation=True):
|
|
msg = ""
|
|
client = Client(api_endpoint)
|
|
msg += "Initialized client"
|
|
# Create account objects
|
|
buyer_private_key = list(self.cipher.decrypt(buyer_encrypted_private_key))
|
|
seller_private_key = list(self.cipher.decrypt(seller_encrypted_private_key))
|
|
assert(len(buyer_private_key) == 32)
|
|
assert(len(seller_private_key) == 32)
|
|
source_account = Account(self.private_key)
|
|
buyer = Account(buyer_private_key)
|
|
seller = Account(seller_private_key)
|
|
# Signers
|
|
signers = [buyer, seller, source_account]
|
|
pool = self.load_binary_option(api_endpoint, pool_account)
|
|
# List non-derived accounts
|
|
pool_account = PublicKey(pool_account)
|
|
escrow_account = PublicKey(pool["escrow"])
|
|
escrow_mint_account = PublicKey(pool["escrow_mint"])
|
|
long_token_mint_account = PublicKey(pool["long_mint"])
|
|
short_token_mint_account = PublicKey(pool["short_mint"])
|
|
buyer_account = buyer.public_key()
|
|
seller_account = seller.public_key()
|
|
token_account = PublicKey(TOKEN_PROGRAM_ID)
|
|
escrow_owner_account = PublicKey.find_program_address(
|
|
[bytes(long_token_mint_account), bytes(short_token_mint_account), bytes(token_account), bytes(PublicKey(BINARY_OPTION_PROGRAM_ID))],
|
|
PublicKey(BINARY_OPTION_PROGRAM_ID),
|
|
)[0]
|
|
# Transaction
|
|
tx = Transaction()
|
|
atas = []
|
|
for acct in [buyer_account, seller_account]:
|
|
acct_atas = []
|
|
for mint_account in (long_token_mint_account, short_token_mint_account, escrow_mint_account):
|
|
token_pda_address = get_associated_token_address(acct, mint_account)
|
|
associated_token_account_info = client.get_account_info(token_pda_address)
|
|
account_info = associated_token_account_info['result']['value']
|
|
if account_info is not None:
|
|
account_state = ACCOUNT_LAYOUT.parse(base64.b64decode(account_info['data'][0])).state
|
|
else:
|
|
account_state = 0
|
|
if account_state == 0:
|
|
msg += f" | Creating PDA: {token_pda_address}"
|
|
associated_token_account_ix = create_associated_token_account(
|
|
payer=source_account.public_key(),
|
|
owner=acct,
|
|
mint=mint_account,
|
|
)
|
|
tx = tx.add(associated_token_account_ix)
|
|
else:
|
|
msg += f" | Fetched PDA: {token_pda_address}"
|
|
acct_atas.append(token_pda_address)
|
|
atas.append(acct_atas)
|
|
trade_ix = trade_instruction(
|
|
pool_account,
|
|
escrow_account,
|
|
long_token_mint_account,
|
|
short_token_mint_account,
|
|
buyer_account,
|
|
seller_account,
|
|
atas[0][2],
|
|
atas[1][2],
|
|
atas[0][0],
|
|
atas[0][1],
|
|
atas[1][0],
|
|
atas[1][1],
|
|
escrow_owner_account,
|
|
token_account,
|
|
int(size),
|
|
int(buyer_price),
|
|
int(seller_price),
|
|
)
|
|
tx = tx.add(trade_ix)
|
|
# Send request
|
|
try:
|
|
response = client.send_transaction(tx, *signers, opts=types.TxOpts(skip_confirmation=skip_confirmation))
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.OK,
|
|
'msg': msg + f" | Trade successful",
|
|
'tx': response.get('result') if skip_confirmation else response['result']['transaction']['signatures'],
|
|
}
|
|
)
|
|
except Exception as e:
|
|
msg += f" | ERROR: Encountered exception while attempting to send transaction: {e}"
|
|
raise(e)
|
|
|
|
def settle(self, api_endpoint, pool_account, winning_mint, skip_confirmation=True):
|
|
msg = ""
|
|
client = Client(api_endpoint)
|
|
msg += "Initialized client"
|
|
# Create account objects
|
|
source_account = Account(self.private_key)
|
|
# Signers
|
|
signers = [source_account]
|
|
# List non-derived accounts
|
|
pool_account = PublicKey(pool_account)
|
|
winning_mint_account = PublicKey(winning_mint)
|
|
tx = Transaction()
|
|
settle_ix = settle_instruction(
|
|
pool_account,
|
|
winning_mint_account,
|
|
source_account.public_key(),
|
|
)
|
|
tx = tx.add(settle_ix)
|
|
# Send request
|
|
try:
|
|
response = client.send_transaction(tx, *signers, opts=types.TxOpts(skip_confirmation=skip_confirmation))
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.OK,
|
|
'msg': msg + f" | Settle successful, winner: {str(winning_mint_account)}",
|
|
'tx': response.get('result') if skip_confirmation else response['result']['transaction']['signatures'],
|
|
}
|
|
)
|
|
except Exception as e:
|
|
msg += f" | ERROR: Encountered exception while attempting to send transaction: {e}"
|
|
raise(e)
|
|
|
|
def collect(self, api_endpoint, pool_account, collector, skip_confirmation=True):
|
|
msg = ""
|
|
client = Client(api_endpoint)
|
|
msg += "Initialized client"
|
|
signers = [Account(self.private_key)]
|
|
pool = self.load_binary_option(api_endpoint, pool_account)
|
|
pool_account = PublicKey(pool_account)
|
|
collector_account = PublicKey(collector)
|
|
escrow_account = PublicKey(pool["escrow"])
|
|
escrow_mint_account = PublicKey(pool["escrow_mint"])
|
|
long_token_mint_account = PublicKey(pool["long_mint"])
|
|
short_token_mint_account = PublicKey(pool["short_mint"])
|
|
token_account = PublicKey(TOKEN_PROGRAM_ID)
|
|
escrow_authority_account = PublicKey.find_program_address(
|
|
[bytes(long_token_mint_account), bytes(short_token_mint_account), bytes(token_account), bytes(PublicKey(BINARY_OPTION_PROGRAM_ID))],
|
|
PublicKey(BINARY_OPTION_PROGRAM_ID),
|
|
)[0]
|
|
# Transaction
|
|
tx = Transaction()
|
|
atas = []
|
|
for mint_account in (long_token_mint_account, short_token_mint_account, escrow_mint_account):
|
|
token_pda_address = get_associated_token_address(collector_account, mint_account)
|
|
associated_token_account_info = client.get_account_info(token_pda_address)
|
|
account_info = associated_token_account_info['result']['value']
|
|
if account_info is not None:
|
|
account_state = ACCOUNT_LAYOUT.parse(base64.b64decode(account_info['data'][0])).state
|
|
else:
|
|
account_state = 0
|
|
if account_state == 0:
|
|
msg += f" | Error Fetching PDA: {token_pda_address}"
|
|
raise Exception()
|
|
else:
|
|
msg += f" | Fetched PDA: {token_pda_address}"
|
|
atas.append(token_pda_address)
|
|
collect_ix = collect_instruction(
|
|
pool_account,
|
|
collector_account,
|
|
atas[0],
|
|
atas[1],
|
|
atas[2],
|
|
long_token_mint_account,
|
|
short_token_mint_account,
|
|
escrow_account,
|
|
escrow_authority_account,
|
|
token_account,
|
|
)
|
|
tx = tx.add(collect_ix)
|
|
try:
|
|
response = client.send_transaction(tx, *signers, opts=types.TxOpts(skip_confirmation=skip_confirmation))
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.OK,
|
|
'msg': msg + f" | Collect successful",
|
|
'tx': response.get('result') if skip_confirmation else response['result']['transaction']['signatures'],
|
|
}
|
|
)
|
|
except Exception as e:
|
|
msg += f" | ERROR: Encountered exception while attempting to send transaction: {e}"
|
|
print(msg)
|
|
raise(e)
|
|
|
|
|
|
def load_binary_option(self, api_endpoint, pool_account):
|
|
client = Client(api_endpoint)
|
|
try:
|
|
pool_data = base64.b64decode(client.get_account_info(pool_account)['result']['value']['data'][0])
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.BAD_REQUEST,
|
|
'msg': str(e),
|
|
}
|
|
)
|
|
pubkey = 'B' * 32
|
|
raw_bytes = struct.unpack(f"<BQ?{pubkey}{pubkey}{pubkey}{pubkey}{pubkey}{pubkey}", pool_data)
|
|
i = 0
|
|
pool = {}
|
|
pool["decimals"] = raw_bytes[i]
|
|
i += 1
|
|
pool["circulation"] = raw_bytes[i]
|
|
i += 1
|
|
pool["settled"] = raw_bytes[i]
|
|
i += 1
|
|
pool["escrow_mint"] = base58.b58encode(bytes(raw_bytes[i:i+32])).decode('ascii')
|
|
i += 32
|
|
pool["escrow"] = base58.b58encode(bytes(raw_bytes[i:i+32])).decode('ascii')
|
|
i += 32
|
|
pool["long_mint"] = base58.b58encode(bytes(raw_bytes[i:i+32])).decode('ascii')
|
|
i += 32
|
|
pool["short_mint"] = base58.b58encode(bytes(raw_bytes[i:i+32])).decode('ascii')
|
|
i += 32
|
|
pool["owner"] = base58.b58encode(bytes(raw_bytes[i:i+32])).decode('ascii')
|
|
i += 32
|
|
pool["winning_side"] = base58.b58encode(bytes(raw_bytes[i:i+32])).decode('ascii')
|
|
i += 32
|
|
return pool
|
|
|
|
def topup(self, api_endpoint, to, amount=None, skip_confirmation=True):
|
|
"""
|
|
Send a small amount of native currency to the specified wallet to handle gas fees. Return a status flag of success or fail and the native transaction data.
|
|
"""
|
|
msg = ""
|
|
try:
|
|
# Connect to the api_endpoint
|
|
client = Client(api_endpoint)
|
|
msg += "Initialized client"
|
|
# List accounts
|
|
sender_account = Account(self.private_key)
|
|
dest_account = PublicKey(to)
|
|
msg += " | Gathered accounts"
|
|
# List signers
|
|
signers = [sender_account]
|
|
# Start transaction
|
|
tx = Transaction()
|
|
# Determine the amount to send
|
|
try:
|
|
if amount is None:
|
|
min_rent_reseponse = client.get_minimum_balance_for_rent_exemption(ACCOUNT_LAYOUT.sizeof())
|
|
lamports = min_rent_reseponse["result"]
|
|
else:
|
|
lamports = int(amount)
|
|
msg += f" | Fetched lamports: {lamports * 1e-9} SOL"
|
|
except Exception as e:
|
|
msg += " | ERROR: couldn't process lamports"
|
|
raise(e)
|
|
# Generate transaction
|
|
transfer_ix = transfer(TransferParams(from_pubkey=sender_account.public_key(), to_pubkey=dest_account, lamports=lamports))
|
|
tx = tx.add(transfer_ix)
|
|
msg += f" | Transferring funds"
|
|
# Send request
|
|
try:
|
|
response = client.send_transaction(tx, *signers, opts=types.TxOpts(skip_confirmation=skip_confirmation))
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.OK,
|
|
'msg': f"Successfully sent {lamports * 1e-9} SOL to {to}",
|
|
'tx': response.get('result') if skip_confirmation else response['result']['transaction']['signatures'],
|
|
}
|
|
)
|
|
except Exception as e:
|
|
msg += f" | ERROR: Encountered exception while attempting to send transaction: {e}"
|
|
raise(e)
|
|
except Exception as e:
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.BAD_REQUEST,
|
|
'msg': msg,
|
|
}
|
|
)
|
|
|
|
def mint_to(self, api_endpoint, pool_account, dest, amount, skip_confirmation=True):
|
|
msg = ""
|
|
client = Client(api_endpoint)
|
|
msg += "Initialized client"
|
|
# Create account objects
|
|
source_account = Account(self.private_key)
|
|
signers = [source_account]
|
|
pool = self.load_binary_option(api_endpoint, pool_account)
|
|
# List non-derived accounts
|
|
pool_account = PublicKey(pool_account)
|
|
dest_account = PublicKey(dest)
|
|
escrow_mint_account = PublicKey(pool["escrow_mint"])
|
|
mint_authority_account = source_account.public_key()
|
|
payer_account = source_account.public_key()
|
|
token_account = PublicKey(TOKEN_PROGRAM_ID)
|
|
tx = Transaction()
|
|
token_pda_address = get_associated_token_address(dest_account, escrow_mint_account)
|
|
associated_token_account_ix = create_associated_token_account(
|
|
payer=payer_account,
|
|
owner=dest_account,
|
|
mint=escrow_mint_account,
|
|
)
|
|
tx = tx.add(associated_token_account_ix)
|
|
mint_to_ix = mint_to(
|
|
MintToParams(
|
|
program_id=token_account,
|
|
mint=escrow_mint_account,
|
|
dest=token_pda_address,
|
|
mint_authority=mint_authority_account,
|
|
amount=int(amount),
|
|
signers=[mint_authority_account],
|
|
)
|
|
)
|
|
tx = tx.add(mint_to_ix)
|
|
# Send request
|
|
try:
|
|
response = client.send_transaction(tx, *signers, opts=types.TxOpts(skip_confirmation=skip_confirmation))
|
|
return json.dumps(
|
|
{
|
|
'status': HTTPStatus.OK,
|
|
'msg': msg + f" | MintTo {dest} successful",
|
|
'tx': response.get('result') if skip_confirmation else response['result']['transaction']['signatures'],
|
|
}
|
|
)
|
|
except Exception as e:
|
|
msg += f" | ERROR: Encountered exception while attempting to send transaction: {e}"
|
|
raise(e)
|