diff --git a/pxsol/program.py b/pxsol/program.py index 4362d0b..29ee03c 100644 --- a/pxsol/program.py +++ b/pxsol/program.py @@ -254,6 +254,77 @@ def extend_program_checked(cls, addi: int) -> bytearray: return pxsol.bincode.Enum.encode(9) + pxsol.bincode.U32.encode(addi) +class LoaderV4: + # The v4 built-in loader program. + # See: https://github.com/anza-xyz/solana-sdk/tree/master/loader-v4-interface + + pubkey = pxsol.core.PubKey.base58_decode('LoaderV411111111111111111111111111111111111') + + # Loader v4 account states size. + size_program_data = 8 + 32 + 8 + + @classmethod + def write(cls, offset: int, data: bytearray) -> bytearray: + # Write elf data into an undeployed program account. Account references: + # 0. -w the program account to write to. + # 1. sr the authority of the program. + return pxsol.bincode.Enum.encode(0) + pxsol.bincode.Struct([ + pxsol.bincode.U32, + pxsol.bincode.Slice(pxsol.bincode.U8), + ]).encode([offset, data]) + + @classmethod + def copy(cls, dst_offset: int, src_offset: int, length: int) -> bytearray: + # Copy elf data into an undeployed program account. Account references: + # 0. -w the program account to write to. + # 1. sr the authority of the program. + # 2. -r the program account to copy from. + return pxsol.bincode.Enum.encode(1) + pxsol.bincode.Struct([ + pxsol.bincode.U32, + pxsol.bincode.U32, + pxsol.bincode.U32, + ]).encode([dst_offset, src_offset, length]) + + @classmethod + def set_program_length(cls, size: int) -> bytearray: + # Changes the size of an undeployed program account. Account references: + # 0. -w the program account to change the size of. + # 1. sr the authority of the program. + # 2. -w optional, the recipient account. + return pxsol.bincode.Enum.encode(2) + pxsol.bincode.U32.encode(size) + + @classmethod + def deploy(cls) -> bytearray: + # Deploy a program account. Account references: + # 0. -w the program account to deploy. + # 1. sr the authority of the program. + # 2. -w optional, an undeployed source program account to take data and lamports from. + return pxsol.bincode.Enum.encode(3) + + @classmethod + def retract(cls) -> bytearray: + # Undo the deployment of a program account. Account references: + # 0. -w the program account to retract. + # 1. sr the authority of the program. + return pxsol.bincode.Enum.encode(4) + + @classmethod + def transfer_authority(cls) -> bytearray: + # Transfers the authority over a program account. Account references: + # 0. -w the program account to change the authority of. + # 1. sr the current authority of the program. + # 2. sr the new authority of the program. + return pxsol.bincode.Enum.encode(5) + + @classmethod + def finalize(cls) -> bytearray: + # Finalizes the program account, rendering it immutable. Account references: + # 0. -w the program account to change the authority of. + # 1. sr the current authority of the program. + # 2. -r the next version of the program (can be itself). + return pxsol.bincode.Enum.encode(6) + + LoaderUpgradeable = LoaderV3 Loader = LoaderV3 diff --git a/pxsol/wallet.py b/pxsol/wallet.py index d0e5ff2..60b8ed8 100644 --- a/pxsol/wallet.py +++ b/pxsol/wallet.py @@ -6,22 +6,13 @@ import typing -class Wallet: - # A built-in solana wallet that can be used to perform most on-chain operations. +class WalletLoaderV3: + # A built-in solana wallet that can be used to perform program loader v3 operations. def __init__(self, prikey: pxsol.core.PriKey) -> None: self.prikey = prikey self.pubkey = prikey.pubkey() - def __repr__(self) -> str: - return json.dumps(self.json()) - - def json(self) -> typing.Dict: - return { - 'prikey': self.prikey.base58(), - 'pubkey': self.pubkey.base58(), - } - def program_buffer(self, bincode: bytearray) -> pxsol.core.PubKey: # Writes a program into a buffer account. The buffer account is randomly generated, and its public key serves # as the function's return value. @@ -156,6 +147,173 @@ def program_update(self, program: pxsol.core.PubKey, bincode: bytearray) -> None txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) pxsol.rpc.wait([txid]) + +class WalletLoaderV4: + # A built-in solana wallet that can be used to perform program loader v4 operations. + + def __init__(self, prikey: pxsol.core.PriKey) -> None: + self.prikey = prikey + self.pubkey = prikey.pubkey() + + def program_closed(self, program: pxsol.core.PubKey) -> None: + # Close a program. The sol allocated to the on-chain program can be fully recovered by performing this action. + r0 = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + r0.account.append(pxsol.core.AccountMeta(program, 1)) + r0.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + r0.data = pxsol.program.LoaderV4.retract() + r1 = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + r1.account.append(pxsol.core.AccountMeta(program, 1)) + r1.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + r1.account.append(pxsol.core.AccountMeta(self.pubkey, 1)) + r1.data = pxsol.program.LoaderV4.set_program_length(0) + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, [r0, r1]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey]) + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + pxsol.rpc.wait([txid]) + + def program_deploy(self, bincode: bytearray) -> pxsol.core.PubKey: + # Deploying a program on solana, returns the program's public key. + tempory_prikey = pxsol.core.PriKey.random() + program = tempory_prikey.pubkey() + pxsol.log.debugln(f'pxsol: program prikey={tempory_prikey}') + pxsol.log.debugln(f'pxsol: program pubkey={program}') + program_real_size = pxsol.program.LoaderV4.size_program_data + len(bincode) + # Create a program account and set its length. + r0 = pxsol.core.Requisition(pxsol.program.System.pubkey, [], bytearray()) + r0.account.append(pxsol.core.AccountMeta(self.pubkey, 3)) + r0.account.append(pxsol.core.AccountMeta(program, 3)) + r0.data = pxsol.program.System.create_account( + pxsol.rpc.get_minimum_balance_for_rent_exemption(program_real_size, {}), + 0, + pxsol.program.LoaderV4.pubkey, + ) + r1 = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + r1.account.append(pxsol.core.AccountMeta(program, 1)) + r1.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + r1.account.append(pxsol.core.AccountMeta(self.pubkey, 1)) + r1.data = pxsol.program.LoaderV4.set_program_length(len(bincode)) + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, [r0, r1]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey, tempory_prikey]) + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + pxsol.rpc.wait([txid]) + # Breaks up the program byte-code into ~1KB chunks and sends transactions to write each chunk with the write + # buffer instruction. + size = 1012 + hall = [] + for i in range(0, len(bincode), size): + elem = bincode[i:i+size] + rq = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + rq.data = pxsol.program.LoaderV4.write(i, elem) + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, [rq]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey]) + assert len(tx.serialize()) <= 1232 + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + hall.append(txid) + pxsol.rpc.wait(hall) + # Deploy. + rq = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + rq.data = pxsol.program.LoaderV4.deploy() + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, [rq]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey]) + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + pxsol.rpc.wait([txid]) + return program + + def program_update(self, program: pxsol.core.PubKey, bincode: bytearray) -> None: + # Updating an existing solana program by new program data and the same program id. + program_info = pxsol.rpc.get_account_info(program.base58(), {}) + assert len(base64.b64decode(program_info['data'][0])) == program_info['space'] + program_real_size = program_info['space'] + program_want_size = pxsol.program.LoaderV4.size_program_data + len(bincode) + rs = [] + rq = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + rq.data = pxsol.program.LoaderV4.retract() + rs.append(rq) + if program_want_size > program_real_size: + addi = pxsol.rpc.get_minimum_balance_for_rent_exemption(program_want_size, {}) - program_info['lamports'] + rq = pxsol.core.Requisition(pxsol.program.System.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 3)) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.data = pxsol.program.System.transfer(addi) + rs.append(rq) + rq = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 1)) + rq.data = pxsol.program.LoaderV4.set_program_length(len(bincode)) + rs.append(rq) + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, rs) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey]) + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + pxsol.rpc.wait([txid]) + # Breaks up the program byte-code into ~1KB chunks and sends transactions to write each chunk with the write + # buffer instruction. + size = 1012 + hall = [] + for i in range(0, len(bincode), size): + elem = bincode[i:i+size] + rq = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + rq.data = pxsol.program.LoaderV4.write(i, elem) + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, [rq]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey]) + assert len(tx.serialize()) <= 1232 + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + hall.append(txid) + pxsol.rpc.wait(hall) + # Deploy + rq = pxsol.core.Requisition(pxsol.program.LoaderV4.pubkey, [], bytearray()) + rq.account.append(pxsol.core.AccountMeta(program, 1)) + rq.account.append(pxsol.core.AccountMeta(self.pubkey, 2)) + rq.data = pxsol.program.LoaderV4.deploy() + tx = pxsol.core.Transaction.requisition_decode(self.pubkey, [rq]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([self.prikey]) + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + pxsol.rpc.wait([txid]) + + +class Wallet: + # A built-in solana wallet that can be used to perform most on-chain operations. + + def __init__(self, prikey: pxsol.core.PriKey) -> None: + self.prikey = prikey + self.pubkey = prikey.pubkey() + + def __repr__(self) -> str: + return json.dumps(self.json()) + + def json(self) -> typing.Dict: + return { + 'prikey': self.prikey.base58(), + 'pubkey': self.pubkey.base58(), + } + + def program_closed(self, program: pxsol.core.PubKey) -> None: + # Close a program. The sol allocated to the on-chain program can be fully recovered by performing this action. + return WalletLoaderV3(self.prikey).program_closed(program) + + def program_deploy(self, bincode: bytearray) -> pxsol.core.PubKey: + # Deploying a program on solana, returns the program's public key. + return WalletLoaderV3(self.prikey).program_deploy(bincode) + + def program_update(self, program: pxsol.core.PubKey, bincode: bytearray) -> None: + # Updating an existing solana program by new program data and the same program id. + return WalletLoaderV3(self.prikey).program_update(program, bincode) + def sol_balance(self) -> int: # Returns the lamport balance of the account. return pxsol.rpc.get_balance(self.pubkey.base58(), {}) diff --git a/test/test_wallet.py b/test/test_wallet.py index a56909c..40b9323 100644 --- a/test/test_wallet.py +++ b/test/test_wallet.py @@ -5,27 +5,54 @@ import typing -def test_program(): +def call_logs(program_pubkey: pxsol.core.PubKey) -> typing.List[str]: user = pxsol.wallet.Wallet(pxsol.core.PriKey.int_decode(1)) + rq = pxsol.core.Requisition(program_pubkey, [], bytearray()) + tx = pxsol.core.Transaction.requisition_decode(user.pubkey, [rq]) + tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) + tx.sign([user.prikey]) + txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) + pxsol.rpc.wait([txid]) + r = pxsol.rpc.get_transaction(txid, {}) + return r['meta']['logMessages'] + + +def test_loader_v3(): + user = pxsol.wallet.WalletLoaderV3(pxsol.core.PriKey.int_decode(1)) program_hello_solana = bytearray(pathlib.Path('res/hello_solana_program.so').read_bytes()) program_hello_update = bytearray(pathlib.Path('res/hello_update_program.so').read_bytes()) + program_pubkey = user.program_deploy(program_hello_solana) + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana!' + user.program_update(program_pubkey, program_hello_update) + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana! Hello Update!' + user.program_update(program_pubkey, program_hello_solana) + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana!' + user.program_closed(program_pubkey) - def call(program_pubkey: pxsol.core.PubKey) -> typing.List[str]: - rq = pxsol.core.Requisition(program_pubkey, [], bytearray()) - tx = pxsol.core.Transaction.requisition_decode(user.pubkey, [rq]) - tx.message.recent_blockhash = pxsol.base58.decode(pxsol.rpc.get_latest_blockhash({})['blockhash']) - tx.sign([user.prikey]) - txid = pxsol.rpc.send_transaction(base64.b64encode(tx.serialize()).decode(), {}) - pxsol.rpc.wait([txid]) - r = pxsol.rpc.get_transaction(txid, {}) - return r['meta']['logMessages'] +def test_loader_v4(): + user = pxsol.wallet.WalletLoaderV3(pxsol.core.PriKey.int_decode(1)) + program_hello_solana = bytearray(pathlib.Path('res/hello_solana_program.so').read_bytes()) + program_hello_update = bytearray(pathlib.Path('res/hello_update_program.so').read_bytes()) + program_pubkey = user.program_deploy(program_hello_solana) + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana!' + user.program_update(program_pubkey, program_hello_update) + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana! Hello Update!' + user.program_update(program_pubkey, program_hello_solana) + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana!' + user.program_closed(program_pubkey) + + +def test_program(): + user = pxsol.wallet.Wallet(pxsol.core.PriKey.int_decode(1)) + program_hello_solana = bytearray(pathlib.Path('res/hello_solana_program.so').read_bytes()) + program_hello_update = bytearray(pathlib.Path('res/hello_update_program.so').read_bytes()) program_pubkey = user.program_deploy(program_hello_solana) - assert call(program_pubkey)[1] == 'Program log: Hello, Solana!' + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana!' user.program_update(program_pubkey, program_hello_update) - assert call(program_pubkey)[1] == 'Program log: Hello, Solana! Hello Update!' + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana! Hello Update!' user.program_update(program_pubkey, program_hello_solana) - assert call(program_pubkey)[1] == 'Program log: Hello, Solana!' + assert call_logs(program_pubkey)[1] == 'Program log: Hello, Solana!' user.program_closed(program_pubkey)