====== Solana study case: Withdrawing LPs from SunnyAG protocol ====== {{tag>dev python crypto nervhome finance solana sunny}} Okay guys, so, that's not quite what I wanted to handle next, but I currently have a bit of a problem with the [[https://app.sunny.ag/|Sunny Aggregator]] platform: I have some funds on the pools there, which I would like to withdraw, but due to a nastly issue with incorrect handling of the "point" in the values that you will enter on the website, you basically cannot withdraw any value with decimals: for instance you can withdraw "7" LPs from a given pool, but not "7." or "7.0", or "7.78", etc. And I guess the dev team is going its best, but that still a bit too slow from my perspective. And on top of that, I've been waiting to jump into solana programming since a long time now, so that sounds like a good opportunity to give it a try here 😊! ====== ====== ===== Gathering infos ===== * The good thing with my journeys on blockchains is that I keep a log of the most important transactions that I do on all blockchains (in a simple text file), so I could easily find the transactions where I withdrawn "integer LP values" on some of the pools that I'm invested in. * Here let's just consider the following 2 transactions: * Withdrawing 7 LPs from the pSol/prtSol pool: [[https://solscan.io/tx/5yzazSqDxyrYqLmjcNtvEXLDiXN8ubFiH7v2Wuci8wLdoKXuAyjJed8VMLkP2zqQngKsceBDeg71T8vz3aGCxf4L|5yzazSqDxyrYqLmjcNtvEXLDiXN8ubFiH7v2Wuci8wLdoKXuAyjJed8VMLkP2zqQngKsceBDeg71T8vz3aGCxf4L]] * Withdrawing 112 LPs from xFTT/wFFT: [[https://solscan.io/tx/MUHWqsXtxQnmFzKE5UNSvfCDiikoihXW64Z9bVcYBsi7rBsML2HNXsaXPtxon7tRvSL2RLFgWfXj2DYVrvhoyTt|MUHWqsXtxQnmFzKE5UNSvfCDiikoihXW64Z9bVcYBsi7rBsML2HNXsaXPtxon7tRvSL2RLFgWfXj2DYVrvhoyTt]] * As I said, I don't have much experience with solana programming yet, but I've done a bit of Rust already, I'm somewhat familiar with Solidity and EVM blockchains, and I've been reading some documentation already on Solana, so I'm not completely lost (yet) with those transactions: * I know for instance that each **Transaction**, is a collection of **Instructions** (cf. https://solanacookbook.com/core-concepts/transactions.html#facts) * And in each Instruction, we will interact with a **program** (given by its ''program_id''), providing an array of **accounts** and the **instruction_data** * In the transactions above, we have 3 instructions each time, * If we scroll down to the **Program Log** section, we can figure out that the instructions are covering those points: * Instruction 1: **UnstakeInternal** * Instruction 2: **WithdrawVendor** * Instruction 3: **WithdrawFromVault** * Now, analyzing the "instruction_data" part, in the first instruction of those transaction we have the data: * 17f74c1e96ead91e00863ba101000000 (for 7 SOL LPs) * 17f74c1e96ead91e0070929b02000000 (for 112 FFT LPs) * => We see that the first part of the data is the same in each case (the common part is **17f74c1e96ead91e**: an 8 byte value, which I would infer is similar to the "method signature" part in solidity: this is used to tell the program "what action we want to perform", and in this case this should represent the "withdraw" action.) * Then I actually spend some time trying to figure out what was the remaining part of the instruction data 😅: * The amount that we want to withdraw is definitely encoded in that data (so "7" in the first tx and "112" in the second tx) * And I was thinking there should also be some kind of "pool id" in that data (because that's typically how this is done on EVM blockchains) * But then I didn't know for sure how many decimals would be used on each pool: so "7" LPs could be represented as 70000000 or 700000000000, or... (again on EVm we usually have 18 decimals, or maybe 9 sometimes... But on solana I had no idea lol) * => Yet, I eventually noticed that at the end of the #1.1 "WithdrawTokens" inner instructions I got the amounts "11200000000" and "7000000000" (So 8 and 9 decimals respectively) * Then things started to fall in place: those "amounts" above were probably coming directly from the instruction data, so I simply converted them to hexadecimal on this page: https://www.rapidtables.com/convert/number/decimal-to-hex.html * And so we get: * 7000000000 -> 0x1A13B8600 * 11200000000 -> 0x29B927000 * => Those values seem "similar" to the second part of the instruction_data in each case, but there are still not quite the same values... until I started to realize the **bytes order** might be swapped in the solana messages 😎! * So splitting the bytes we get: * 7000000000 -> 0x 01 A1 3B 86 00 * 11200000000 -> 0x 02 9B 92 70 00 * And now if we invert the order of those bytes: * 7000000000 -> 00 86 3B A1 01 * 11200000000 -> 00 70 92 9B 02 * And **bingo**, this time, we see that this is precisely the values that are passed in the second part of the instruction data: * 17f74c1e96ead91e**00863ba101**000000 * 17f74c1e96ead91e**0070929b02**000000 * So this is where our "amount" we want to withdraw comes from, and we also note that there is nothing else passed in that instruction data: so my expectation of a **pool id** here was wrong. I suspect that instead the "pool" of interest is selected from one of the **accounts** also provided as part of the full instruction. * And this is it for the **first instruction**, next we have the **second instruction**: * Still interacting with the same program as in the first instruction (SPQR4kT3q2oUKEJes2L6NNSBCiPW9SfuhkuqC9bp6Sx) * but this time with the instruction data: * 39eabc535ce92cc700863ba101000000 ( for 7 SOL Lps) * 39eabc535ce92cc70070929b02000000 ( for 112 FFT Lps) * The "amounts" parts are the same as in the first instruction here, only the "function signature" part is changed, OK: that's not a problem. * And for the **third instruction**, we only have the instruction data (same in both cases): * b422252e9c00d3ee * b422252e9c00d3ee * => So again, this seems to be a simple "function signature" for a function with 0 arguments => Should be OK too! * => So with this knowledge in place, we should in theory be able to interact with that program manually, by building the transaction we want just providing the correct "amount" bytes in the instruction data(s) (? ðŸĪŠ or maybe I'm just crazy lol). Let's see if I can do that. ===== Initial sunny withdrawal python script ===== * As usual I'm building this as a minimal component using my [[https://github.com/roche-emmanuel/nervproj|NervProj framework]]: """Module for SunnyAg class definition""" import logging from nvp.nvp_context import NVPContext from nvp.nvp_component import NVPComponent logger = logging.getLogger(__name__) class SunnyAg(NVPComponent): """SunnyAg component class""" def __init__(self, ctx: NVPContext): """class constructor""" NVPComponent.__init__(self, ctx) def process_command(self, cmd): """Check if this component can process the given command""" if cmd == 'withdraw': logger.info("Should withdraw funds here.") return True return False if __name__ == "__main__": # Create the context: context = NVPContext() # Add our component: comp = context.register_component("SunnyAg", SunnyAg(context)) context.define_subparsers("main", { 'withdraw': None, }) comp.run() * I then prepare a dedicate python env: "solana_env": { "packages": ["requests", "jstyleson", "xxhash", "solana", "numpy"] } * And a script to run that component: "sunnyag": { "custom_python_env": "solana_env", "cmd": "${PYTHON} ${PROJECT_ROOT_DIR}/nvh/crypto/sunny_ag.py", "python_path": ["${PROJECT_ROOT_DIR}", "${NVP_ROOT_DIR}"] } * So now I can run the command: $ nvp sunnyag withdraw 2022/05/19 19:42:48 [__main__] INFO: Should withdraw funds here. ===== Building the transaction ===== * Okay, now time to try to interact with that solana program 😅... * I found this page with a nice example building a transaction in python: https://stackoverflow.com/questions/71640223/interact-with-solana-program-directly * So let's see how far I can go with that! In fact I also need to install the **base58** package in my python env * Youuuhoooo!! It's working ðŸĪŠ! * So here is the initial implementation I tested: """Module for SunnyAg class definition""" import logging from getpass import getpass from solana.transaction import AccountMeta, Transaction, TransactionInstruction from solana.rpc.types import TxOpts from solana.rpc.api import Client from solana.publickey import PublicKey from solana.keypair import Keypair # from solana.account import Account # from solana.rpc.commitment import Recent, Root # from solana.system_program import create_account, CreateAccountParams # from spl.token.instructions import set_authority, SetAuthorityParams, AuthorityType import base58 from nvp.nvp_context import NVPContext from nvp.nvp_component import NVPComponent logger = logging.getLogger(__name__) class SunnyAg(NVPComponent): """SunnyAg component class""" def __init__(self, ctx: NVPContext): """class constructor""" NVPComponent.__init__(self, ctx) def withdraw_funds(self): """method used to withdraw our funds from a given pool.""" url = 'https://api.mainnet-beta.solana.com' client = Client(url) pwd = getpass('Chrome -> Phantom -> Settings -> Export private Key -> paste it here: ') # cf. https://solscan.io/tx/MUHWqsXtxQnmFzKE5UNSvfCDiikoihXW64Z9bVcYBsi7rBsML2HNXsaXPtxon7tRvSL2RLFgWfXj2DYVrvhoyTt # as reference for the setup below: # id of the program: program = 'SPQR4kT3q2oUKEJes2L6NNSBCiPW9SfuhkuqC9bp6Sx' # get int based keypair of account byte_array = base58.b58decode(pwd) keypair = list(map(lambda b: int(str(b)), byte_array))[:] initializer_account = Keypair(keypair[0:32]) logger.info("My public key is: %s", initializer_account.public_key) txn = Transaction(recent_blockhash=client.get_recent_blockhash()[ 'result']['value']['blockhash'], fee_payer=initializer_account.public_key) # First instruction: # I have 0.785886118 LPs left, let's start with only 0.001 just in case. # => We have 9 decimals for that one, so the amount will be 0.001 * 1e9 = 1000000 # We convert 1000000 into hex: 0xF4240 -> 0x 0F 42 40 # Then we invert the bytes: 40 42 0F # So we build the instruction data: # ref: "17f74c1e96ead91e00863ba101000000" idata1 = "17f74c1e96ead91e40420f0000000000" txn.add( TransactionInstruction( keys=[ AccountMeta(pubkey=PublicKey(initializer_account.public_key), # 1 is_signer=True, is_writable=True), AccountMeta( pubkey=PublicKey('5mLQTfsdDhXF7s7KYXYcKNSBWxpFVScvLUsi2F6fATMD'), # 2 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('2Mpg4g8dVseygxyRiTg9iXPWuXHWJU9xdcKZdrbYexGx'), # 3 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN'), # 4 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte'), # 5 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('97PmYbGpSHSrKrUkQX793mjpA2EA9rrQKkHsQuvenU44'), # 6 is_signer=False, is_writable=False), # Note: not writable. AccountMeta( pubkey=PublicKey('EuQRwGreQZ56tfHrB1dvQfkm8gYkKp6HPbTK1Z6fGZhy'), # 7 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('54Ex5fMemkZVuCDyAB1pxBARnUPGwAyaoJZWDdoX3LPR'), # 8 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('2xs3pAHoSwrkQbcYaEwhqtgDPpNcK9akQdUqZ8r763nS'), # 9 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), # 10 is_signer=False, is_writable=False), AccountMeta( pubkey=PublicKey('QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB'), # 11 is_signer=False, is_writable=False), ], program_id=PublicKey(program), data=bytes.fromhex(idata1) ) ) # Second instruction: # we build the instruction data: # ref: "39eabc535ce92cc700863ba101000000" idata2 = "39eabc535ce92cc740420f0000000000" txn.add( TransactionInstruction( keys=[ AccountMeta(pubkey=PublicKey(initializer_account.public_key), # 1 is_signer=True, is_writable=True), AccountMeta( pubkey=PublicKey('HRavzHF4bymJ59BGgEVRvNSx74zq7ceD2Dwxb9fMvZ87'), # 2 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN'), # 3 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte'), # 4 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('rXhAofQCT7NN9TUqigyEAUzV1uLL4boeD8CRkNBSkYk'), # 5 is_signer=False, is_writable=False), # Note: not writable. AccountMeta( pubkey=PublicKey('8uEjJsJ5cCbz7m4K9jZEmQB6cL3SM3V2suc6fazkgWan'), # 6 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('7DcyStUnaDVySSMz9aFucgnKEQxtFbELzRY1gk5vTMhw'), # 7 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('CSpYqVQx1cZRJdApjuMSyNqB3mUrC6ify5u7NYzVEK8S'), # 8 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), # 9 is_signer=False, is_writable=False), AccountMeta( pubkey=PublicKey('QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB'), # 10 is_signer=False, is_writable=False), ], program_id=PublicKey(program), data=bytes.fromhex(idata2) ) ) # Third instruction: # we build the instruction data: # ref: "b422252e9c00d3ee" idata3 = "b422252e9c00d3ee" txn.add( TransactionInstruction( keys=[ AccountMeta(pubkey=PublicKey(initializer_account.public_key), # 1 is_signer=True, is_writable=True), AccountMeta( pubkey=PublicKey('D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN'), # 2 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte'), # 3 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('HRavzHF4bymJ59BGgEVRvNSx74zq7ceD2Dwxb9fMvZ87'), # 4 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('7TuJswHsetUW11fBLQSHe4msTRRGB1KDGL9XBa5v7ur6'), # 5 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('HiEYoocRJtrRREuDLyXVBYm3mNAEscGyw1PGJ6jmn6JE'), # 6 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), # 7 is_signer=False, is_writable=False), ], program_id=PublicKey(program), data=bytes.fromhex(idata3) ) ) # sign and send logger.info("Sending transaction...") txn.sign(initializer_account) rpc_response = client.send_transaction( txn, initializer_account, opts=TxOpts(skip_preflight=True, skip_confirmation=False) ) logger.info("Got RPC response: %s", rpc_response) def process_command(self, cmd): """Check if this component can process the given command""" if cmd == 'withdraw': logger.info("Trying to withdraw funds...") self.withdraw_funds() return True return False if __name__ == "__main__": # Create the context: context = NVPContext() # Add our component: comp = context.register_component("SunnyAg", SunnyAg(context)) context.define_subparsers("main", { 'withdraw': None, }) comp.run() * => As described in the comments I'm just trying to withdraw .001 LPs from the pSOL/prtSOL pool for now. * Then I run the command (pasting my private key when requested at start), and I got the following result: $ nvp sunnyag withdraw 2022/05/19 20:43:05 [__main__] INFO: Trying to withdraw funds... Chrome -> Phantom -> Settings -> Export private Key -> paste it here: 2022/05/19 20:43:40 [__main__] INFO: My public key is: 7X2mFEFeddSsK2WMCP3rqv7fzLPs6MLCSG1GWEmGzfDn 2022/05/19 20:43:41 [__main__] INFO: Sending transaction... 2022/05/19 20:43:41 [solanaweb3.rpc.httprpc.HTTPClient] INFO: Transaction sent to https://api.mainnet-beta.solana.com. Signature W64TEFZrvcnBcpTeoxg6Qvqb6ixq7PoNtdkP5b5qp qAUCpMcp4R9PwzqtYMcSjT7Nc4LRYtyrVShgbopokkMuYA: Traceback (most recent call last): File "D:\Projects\NervHome\nvh\crypto\sunny_ag.py", line 245, in comp.run() File "D:\Projects\NervProj\nvp\nvp_component.py", line 69, in run res = self.process_command(cmd) File "D:\Projects\NervHome\nvh\crypto\sunny_ag.py", line 228, in process_command self.withdraw_funds() File "D:\Projects\NervHome\nvh\crypto\sunny_ag.py", line 215, in withdraw_funds rpc_response = client.send_transaction( File "D:\Projects\NervProj\.pyenvs\solana_env\lib\site-packages\solana\rpc\api.py", line 1281, in send_transaction txn_resp = self.send_raw_transaction(txn.serialize(), opts=opts) File "D:\Projects\NervProj\.pyenvs\solana_env\lib\site-packages\solana\rpc\api.py", line 1235, in send_raw_transaction return self.__post_send_with_confirm(*post_send_args) File "D:\Projects\NervProj\.pyenvs\solana_env\lib\site-packages\solana\rpc\api.py", line 1347, in __post_send_with_confirm self.confirm_transaction(resp["result"], conf_comm) File "D:\Projects\NervProj\.pyenvs\solana_env\lib\site-packages\solana\rpc\api.py", line 1378, in confirm_transaction raise UnconfirmedTxError(f"Unable to confirm transaction {tx_sig}") solana.rpc.core.UnconfirmedTxError: Unable to confirm transaction W64TEFZrvcnBcpTeoxg6Qvqb6ixq7PoNtdkP5b5qpqAUCpMcp4R9PwzqtYMcSjT7Nc4LRYtyrVShgbopokkMuYA Traceback (most recent call last): File "D:\Projects\NervProj\cli.py", line 5, in ctx.run() File "D:\Projects\NervProj\nvp\nvp_context.py", line 403, in run if comp.process_command(cmd): File "D:\Projects\NervProj\nvp\components\runner.py", line 42, in process_command self.run_script(sname, proj) File "D:\Projects\NervProj\nvp\components\runner.py", line 155, in run_script self.execute(cmd, cwd=cwd, env=env) File "D:\Projects\NervProj\nvp\nvp_object.py", line 422, in execute subprocess.check_call(cmd, stdout=stdout, stderr=stderr, cwd=cwd, env=env) File "D:\Projects\NervProj\tools\windows\python-3.10.1\lib\subprocess.py", line 369, in check_call raise CalledProcessError(retcode, cmd) subprocess.CalledProcessError: Command '['D:\\Projects\\NervProj\\.pyenvs\\solana_env\\python.exe', 'D:\\Projects\\NervHome/nvh/crypto/sunny_ag.py', 'withdraw']' returned non-zero exit status 1. * Okay okay, this is ending lamentably with an exception, I know, but this is only because the transaction could not be confirmed before the default timeout. * In fact, when checking the transaction a few moments later, everything seems OK! see it [[+tab|https://solscan.io/tx/W64TEFZrvcnBcpTeoxg6Qvqb6ixq7PoNtdkP5b5qpqAUCpMcp4R9PwzqtYMcSjT7Nc4LRYtyrVShgbopokkMuYA|here on solscan]]. ===== Withdrawing the remaining LPs ===== * Okay now that this initial version working, let's continue and remove the remaining LPs, * And here is the second version of this script: """Module for SunnyAg class definition""" import logging import os import time # from getpass import getpass from solana.transaction import AccountMeta, Transaction, TransactionInstruction from solana.rpc.types import TxOpts from solana.rpc.api import Client from solana.publickey import PublicKey from solana.keypair import Keypair from solana.rpc.core import UnconfirmedTxError # from solana.account import Account # from solana.rpc.commitment import Recent, Root # from solana.system_program import create_account, CreateAccountParams # from spl.token.instructions import set_authority, SetAuthorityParams, AuthorityType import base58 from nvp.nvp_context import NVPContext from nvp.nvp_component import NVPComponent logger = logging.getLogger(__name__) class SunnyAg(NVPComponent): """SunnyAg component class""" def __init__(self, ctx: NVPContext): """class constructor""" NVPComponent.__init__(self, ctx) def withdraw_funds(self, amount): """method used to withdraw our funds from a given pool.""" url = 'https://api.mainnet-beta.solana.com' client = Client(url) # pwd = getpass('Chrome -> Phantom -> Settings -> Export private Key -> paste it here: ') priv_key = os.getenv('PHANTOM_KEY') assert priv_key is not None, "Phantom private key not set." # cf. https://solscan.io/tx/MUHWqsXtxQnmFzKE5UNSvfCDiikoihXW64Z9bVcYBsi7rBsML2HNXsaXPtxon7tRvSL2RLFgWfXj2DYVrvhoyTt # as reference for the setup below: # id of the program: program = 'SPQR4kT3q2oUKEJes2L6NNSBCiPW9SfuhkuqC9bp6Sx' # get int based keypair of account byte_array = base58.b58decode(priv_key) keypair = list(map(lambda b: int(str(b)), byte_array))[:] initializer_account = Keypair(keypair[0:32]) logger.info("My public key is: %s", initializer_account.public_key) txn = Transaction(recent_blockhash=client.get_recent_blockhash()[ 'result']['value']['blockhash'], fee_payer=initializer_account.public_key) # First instruction: # I have 0.785886118 LPs left, let's start with only 0.001 just in case. # => We have 9 decimals for that one, so the amount will be 0.001 * 1e9 = 1000000 # We convert 1000000 into hex: 0xF4240 -> 0x 0F 42 40 # Then we invert the bytes: 40 42 0F # So we build the instruction data: # ref: "17f74c1e96ead91e00863ba101000000" # idata1 = "17f74c1e96ead91e40420f0000000000" # Update: # Below we compute the amount representation from the integer value provided as # argument (Note: integer value because we assume the decimal multiplier is already # taken into account) # convert the amount to key (we remove the "0x" prefix) val = hex(amount)[2:] # add a prefix 0 if the number of char is odd: if len(val) % 2 != 0: val = f"0{val}" # Now we split in bytes: bval = [val[i:i+2] for i in range(0, len(val), 2)] bval.reverse() amount_rep = "".join(bval) # the value we send should take 16 characters, so we add the required 0 at the end: left = 16 - len(amount_rep) amount_rep += "0"*left logger.info("Using amount representation: %s", amount_rep) idata1 = f"17f74c1e96ead91e{amount_rep}" txn.add( TransactionInstruction( keys=[ AccountMeta(pubkey=PublicKey(initializer_account.public_key), # 1 is_signer=True, is_writable=True), AccountMeta( pubkey=PublicKey('5mLQTfsdDhXF7s7KYXYcKNSBWxpFVScvLUsi2F6fATMD'), # 2 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('2Mpg4g8dVseygxyRiTg9iXPWuXHWJU9xdcKZdrbYexGx'), # 3 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN'), # 4 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte'), # 5 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('97PmYbGpSHSrKrUkQX793mjpA2EA9rrQKkHsQuvenU44'), # 6 is_signer=False, is_writable=False), # Note: not writable. AccountMeta( pubkey=PublicKey('EuQRwGreQZ56tfHrB1dvQfkm8gYkKp6HPbTK1Z6fGZhy'), # 7 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('54Ex5fMemkZVuCDyAB1pxBARnUPGwAyaoJZWDdoX3LPR'), # 8 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('2xs3pAHoSwrkQbcYaEwhqtgDPpNcK9akQdUqZ8r763nS'), # 9 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), # 10 is_signer=False, is_writable=False), AccountMeta( pubkey=PublicKey('QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB'), # 11 is_signer=False, is_writable=False), ], program_id=PublicKey(program), data=bytes.fromhex(idata1) ) ) # Second instruction: # we build the instruction data: # ref: "39eabc535ce92cc700863ba101000000" # idata2 = "39eabc535ce92cc740420f0000000000" idata2 = f"39eabc535ce92cc7{amount_rep}" txn.add( TransactionInstruction( keys=[ AccountMeta(pubkey=PublicKey(initializer_account.public_key), # 1 is_signer=True, is_writable=True), AccountMeta( pubkey=PublicKey('HRavzHF4bymJ59BGgEVRvNSx74zq7ceD2Dwxb9fMvZ87'), # 2 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN'), # 3 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte'), # 4 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('rXhAofQCT7NN9TUqigyEAUzV1uLL4boeD8CRkNBSkYk'), # 5 is_signer=False, is_writable=False), # Note: not writable. AccountMeta( pubkey=PublicKey('8uEjJsJ5cCbz7m4K9jZEmQB6cL3SM3V2suc6fazkgWan'), # 6 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('7DcyStUnaDVySSMz9aFucgnKEQxtFbELzRY1gk5vTMhw'), # 7 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('CSpYqVQx1cZRJdApjuMSyNqB3mUrC6ify5u7NYzVEK8S'), # 8 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), # 9 is_signer=False, is_writable=False), AccountMeta( pubkey=PublicKey('QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB'), # 10 is_signer=False, is_writable=False), ], program_id=PublicKey(program), data=bytes.fromhex(idata2) ) ) # Third instruction: # we build the instruction data: # ref: "b422252e9c00d3ee" idata3 = "b422252e9c00d3ee" txn.add( TransactionInstruction( keys=[ AccountMeta(pubkey=PublicKey(initializer_account.public_key), # 1 is_signer=True, is_writable=True), AccountMeta( pubkey=PublicKey('D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN'), # 2 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte'), # 3 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('HRavzHF4bymJ59BGgEVRvNSx74zq7ceD2Dwxb9fMvZ87'), # 4 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('7TuJswHsetUW11fBLQSHe4msTRRGB1KDGL9XBa5v7ur6'), # 5 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('HiEYoocRJtrRREuDLyXVBYm3mNAEscGyw1PGJ6jmn6JE'), # 6 is_signer=False, is_writable=True), AccountMeta( pubkey=PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'), # 7 is_signer=False, is_writable=False), ], program_id=PublicKey(program), data=bytes.fromhex(idata3) ) ) # sign and send logger.info("Sending transaction...") txn.sign(initializer_account) rpc_response = client.send_transaction( txn, initializer_account, opts=TxOpts(skip_preflight=True, skip_confirmation=True) ) # the transaction hash should be in the 'result' key: logger.info("Got RPC response: %s", rpc_response) txhash = rpc_response['result'] count = 0 # wait a short moment: time.sleep(5.0) max_count = 10 while count < max_count: # cf. https://docs.solana.com/developing/clients/jsonrpc-api#getsignaturestatuses res = client.get_signature_statuses([txhash]) if res[0] is None: logger.info("Transaction %s is unknown.", txhash) count += 1 else: # we have an object: obj = res[0] status = obj.get('confirmationStatus', None) if status == "confirmed": logger.info("Transaction %s is confirmed!", txhash) break else: logger.info("Transaction status: %s", self.pretty_print(obj)) count += 1 logger.info("Waiting for confirmation...") # Wait 1 minute: time.sleep(60) if count == max_count: logger.warning("Could not confirm transaction %s, please check manually.", txhash) def process_command(self, cmd): """Check if this component can process the given command""" if cmd == 'withdraw': logger.info("Trying to withdraw funds...") amount = self.get_param("amount") self.withdraw_funds(amount) return True return False if __name__ == "__main__": # Create the context: context = NVPContext() # Add our component: comp = context.register_component("SunnyAg", SunnyAg(context)) context.define_subparsers("main", { 'withdraw': None, }) psr = context.get_parser('main.withdraw') psr.add_argument("amount", type=int, help="Amount to withdraw") comp.run() * Again, running this script worked (resulting in a valid transaction), but reported an exception in the confirmation process: $ nvp sunnyag withdraw 784886118 2022/05/19 22:03:33 [__main__] INFO: Trying to withdraw funds... 2022/05/19 22:03:33 [__main__] INFO: My public key is: 7X2mFEFeddSsK2WMCP3rqv7fzLPs6MLCSG1GWEmGzfDn 2022/05/19 22:03:34 [__main__] INFO: Using amount representation: 6669c82e00000000 2022/05/19 22:03:34 [__main__] INFO: Sending transaction... 2022/05/19 22:03:35 [__main__] INFO: Got RPC response: {'jsonrpc': '2.0', 'result': '3YxVCFBHAYoNEsmg962spiZTNLthzLLvz92g9CRCW2H6fhtSauBv8EncTQvYfQdrLf6Y4HsyJbWPpa9X1fZnwu9p', 'id': 3} 2022/05/19 22:03:35 [__main__] INFO: Waiting for confirmation... Traceback (most recent call last): File "D:\Projects\NervHome\nvh\crypto\sunny_ag.py", line 278, in comp.run() File "D:\Projects\NervProj\nvp\nvp_component.py", line 69, in run res = self.process_command(cmd) File "D:\Projects\NervHome\nvh\crypto\sunny_ag.py", line 257, in process_command self.withdraw_funds(amount) File "D:\Projects\NervHome\nvh\crypto\sunny_ag.py", line 234, in withdraw_funds if res[0] is None: KeyError: 0 Traceback (most recent call last): File "D:\Projects\NervProj\cli.py", line 5, in ctx.run() File "D:\Projects\NervProj\nvp\nvp_context.py", line 403, in run if comp.process_command(cmd): File "D:\Projects\NervProj\nvp\components\runner.py", line 42, in process_command self.run_script(sname, proj) File "D:\Projects\NervProj\nvp\components\runner.py", line 155, in run_script self.execute(cmd, cwd=cwd, env=env) File "D:\Projects\NervProj\nvp\nvp_object.py", line 422, in execute subprocess.check_call(cmd, stdout=stdout, stderr=stderr, cwd=cwd, env=env) File "D:\Projects\NervProj\tools\windows\python-3.10.1\lib\subprocess.py", line 369, in check_call raise CalledProcessError(retcode, cmd) subprocess.CalledProcessError: Command '['D:\\Projects\\NervProj\\.pyenvs\\solana_env\\python.exe', 'D:\\Projects\\NervHome/nvh/crypto/sunny_ag.py', 'withdraw', '7848861 18']' returned non-zero exit status 1. * => That must be because I'm not access the 'res' object correctly above, will fix that on the next iteration. ===== Withdrawing LPs from another pool ===== * Now that I could remove all my remaining LPs from the pSOL/prtSOL pool, let's try to do the same for the xFFT/wFFT pool. * => So here is the updated version of the script: """Module for SunnyAg class definition""" import logging import os import time # from getpass import getpass from solana.transaction import AccountMeta, Transaction, TransactionInstruction from solana.rpc.types import TxOpts from solana.rpc.api import Client from solana.publickey import PublicKey from solana.keypair import Keypair # from solana.account import Account # from solana.rpc.commitment import Recent, Root # from solana.system_program import create_account, CreateAccountParams # from spl.token.instructions import set_authority, SetAuthorityParams, AuthorityType import base58 from nvp.nvp_context import NVPContext from nvp.nvp_component import NVPComponent logger = logging.getLogger(__name__) # Listing of all accounts used as inputs to withdraw from a given sunnyag pool: # **Note**: an exclamation mark at the beginning of the account key means that # the account will be used in readonly mode (ie. not writable) when building the # transaction instruction below. ALL_ACCOUNTS = { "pSOL_prtSOL": [ [ "5mLQTfsdDhXF7s7KYXYcKNSBWxpFVScvLUsi2F6fATMD", "2Mpg4g8dVseygxyRiTg9iXPWuXHWJU9xdcKZdrbYexGx", "D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN", "4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte", "!97PmYbGpSHSrKrUkQX793mjpA2EA9rrQKkHsQuvenU44", "EuQRwGreQZ56tfHrB1dvQfkm8gYkKp6HPbTK1Z6fGZhy", "54Ex5fMemkZVuCDyAB1pxBARnUPGwAyaoJZWDdoX3LPR", "2xs3pAHoSwrkQbcYaEwhqtgDPpNcK9akQdUqZ8r763nS", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "!QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", ], [ "HRavzHF4bymJ59BGgEVRvNSx74zq7ceD2Dwxb9fMvZ87", "D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN", "4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte", "!rXhAofQCT7NN9TUqigyEAUzV1uLL4boeD8CRkNBSkYk", "8uEjJsJ5cCbz7m4K9jZEmQB6cL3SM3V2suc6fazkgWan", "7DcyStUnaDVySSMz9aFucgnKEQxtFbELzRY1gk5vTMhw", "CSpYqVQx1cZRJdApjuMSyNqB3mUrC6ify5u7NYzVEK8S", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "!QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", ], [ "D7FXWcnRGRrxypYZvujosegypQ9MsZ6cZMoh8qAJVmqN", "4uKud9gWbseDAyENU3dnei21rFwRbXofgSsvScFNUbte", "HRavzHF4bymJ59BGgEVRvNSx74zq7ceD2Dwxb9fMvZ87", "7TuJswHsetUW11fBLQSHe4msTRRGB1KDGL9XBa5v7ur6", "HiEYoocRJtrRREuDLyXVBYm3mNAEscGyw1PGJ6jmn6JE", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", ] ], "xFTT_wFTT": [ [ "F7tt5sohvc1D4dvn2cmJkD2mR6cwtujWG2naKX8x7L7o", "FizkdyUeTRs8XYhA7v9d1XjQ4U25YUx7igJn6xQoXu76", "Ex33MaUfcdDUKfsdxsLWCqy4zEheQcLs8X45gBaiKJgF", "GYrfqfN6n1csWgzQTvChYNBGduddjfCQnMT6w6eKEseQ", "!97PmYbGpSHSrKrUkQX793mjpA2EA9rrQKkHsQuvenU44", "EuLpc9s1caxWetHBLRxnetQUKkAygngRb1PgYA8qwK1S", "5Y2RrrgNaD9tJYyGeqQ2zp9d1BJP6qEH46U2JyrSteQ4", "4U86Cm3twisfWYwFuEPRLBWJdQGkSu5V67Jg9makygG7", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "!QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", ], [ "3UnZAhERbCB8NRhxnVEpXh8mJGha99RJv79tRZGw4NSo", "Ex33MaUfcdDUKfsdxsLWCqy4zEheQcLs8X45gBaiKJgF", "GYrfqfN6n1csWgzQTvChYNBGduddjfCQnMT6w6eKEseQ", "!rXhAofQCT7NN9TUqigyEAUzV1uLL4boeD8CRkNBSkYk", "FdHVdkarMfbpPhiAhHtKK6PgH8ibN8SnGSqWhQkTyD2c", "EF39s4uGKyPgqgmJVrv8UvmpKGSoKdVSkpT4GDPHo59i", "99SQokGqorXikQ52cofLRX9kFx475Fg3X57EvVusqNEZ", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "!QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", ], [ "Ex33MaUfcdDUKfsdxsLWCqy4zEheQcLs8X45gBaiKJgF", "GYrfqfN6n1csWgzQTvChYNBGduddjfCQnMT6w6eKEseQ", "3UnZAhERbCB8NRhxnVEpXh8mJGha99RJv79tRZGw4NSo", "2dzG4gXCkAaCpSm7swCrADE6qfVbt2wquyby23NGa35V", "AZcbkuFqeTMacxd1GBcMypqqX19YfWL1i9dMxneqgu2W", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", ] ] } class SunnyAg(NVPComponent): """SunnyAg component class""" def __init__(self, ctx: NVPContext): """class constructor""" NVPComponent.__init__(self, ctx) def withdraw_funds(self, amount, pool_name): """method used to withdraw our funds from a given pool.""" url = 'https://api.mainnet-beta.solana.com' client = Client(url) # pwd = getpass('Chrome -> Phantom -> Settings -> Export private Key -> paste it here: ') priv_key = os.getenv('PHANTOM_KEY') assert priv_key is not None, "Phantom private key not set." # cf. https://solscan.io/tx/MUHWqsXtxQnmFzKE5UNSvfCDiikoihXW64Z9bVcYBsi7rBsML2HNXsaXPtxon7tRvSL2RLFgWfXj2DYVrvhoyTt # as reference for the setup below: # id of the program: program = 'SPQR4kT3q2oUKEJes2L6NNSBCiPW9SfuhkuqC9bp6Sx' # get int based keypair of account byte_array = base58.b58decode(priv_key) keypair = list(map(lambda b: int(str(b)), byte_array))[:] initializer_account = Keypair(keypair[0:32]) logger.info("My public key is: %s", initializer_account.public_key) txn = Transaction(recent_blockhash=client.get_recent_blockhash()[ 'result']['value']['blockhash'], fee_payer=initializer_account.public_key) # First instruction: # I have 0.785886118 LPs left, let's start with only 0.001 just in case. # => We have 9 decimals for that one, so the amount will be 0.001 * 1e9 = 1000000 # We convert 1000000 into hex: 0xF4240 -> 0x 0F 42 40 # Then we invert the bytes: 40 42 0F # So we build the instruction data: # ref: "17f74c1e96ead91e00863ba101000000" # idata1 = "17f74c1e96ead91e40420f0000000000" # Update: # Below we compute the amount representation from the integer value provided as # argument (Note: integer value because we assume the decimal multiplier is already # taken into account) # convert the amount to key (we remove the "0x" prefix) val = hex(amount)[2:] # add a prefix 0 if the number of char is odd: if len(val) % 2 != 0: val = f"0{val}" # Now we split in bytes: bval = [val[i:i+2] for i in range(0, len(val), 2)] bval.reverse() amount_rep = "".join(bval) # the value we send should take 16 characters, so we add the required 0 at the end: left = 16 - len(amount_rep) amount_rep += "0"*left logger.info("Using amount representation: %s", amount_rep) logger.info("Pool name: %s", pool_name) assert pool_name in ALL_ACCOUNTS, "Invalid pool name" accounts = ALL_ACCOUNTS[pool_name] def build_inputs(addrs): """Build the inputs from a list of addresses""" inputs = [AccountMeta(pubkey=PublicKey(initializer_account.public_key), is_signer=True, is_writable=True)] for addr in addrs: write = True if addr[0] == "!": write = False addr = addr[1:] # logger.info("Making %s read only", addr) inputs.append(AccountMeta(pubkey=PublicKey(addr), is_signer=False, is_writable=write)) return inputs txn.add( TransactionInstruction( keys=build_inputs(accounts[0]), program_id=PublicKey(program), data=bytes.fromhex(f"17f74c1e96ead91e{amount_rep}") ) ) # Second instruction: txn.add( TransactionInstruction( keys=build_inputs(accounts[1]), program_id=PublicKey(program), data=bytes.fromhex(f"39eabc535ce92cc7{amount_rep}") ) ) # Third instruction: txn.add( TransactionInstruction( keys=build_inputs(accounts[2]), program_id=PublicKey(program), data=bytes.fromhex("b422252e9c00d3ee") ) ) # sign and send logger.info("Sending transaction...") txn.sign(initializer_account) rpc_response = client.send_transaction( txn, initializer_account, opts=TxOpts(skip_preflight=False, skip_confirmation=True) ) logger.info("Got RPC response: %s", rpc_response) txhash = rpc_response['result'] count = 0 # wait a short moment: time.sleep(15.0) max_count = 10 while count < max_count: # cf. https://docs.solana.com/developing/clients/jsonrpc-api#getsignaturestatuses res = client.get_signature_statuses([txhash]) arr = res['result']['value'] if arr[0] is None: logger.info("Transaction %s is unknown.", txhash) count += 1 else: # we have an object: obj = arr[0] status = obj.get('confirmationStatus', None) if status == "finalized": logger.info("Transaction %s is finalized!", txhash) break else: logger.info("Transaction status: %s", self.pretty_print(obj)) count += 1 logger.info("Waiting for confirmation...") # Wait 30 seconds: time.sleep(30) if count == max_count: logger.warning("Could not confirm transaction %s, please check manually.", txhash) def process_command(self, cmd): """Check if this component can process the given command""" if cmd == 'withdraw': logger.info("Trying to withdraw funds...") amount = self.get_param("amount") pool_name = self.get_param("pool_name") self.withdraw_funds(amount, pool_name) return True return False if __name__ == "__main__": # Create the context: context = NVPContext() # Add our component: comp = context.register_component("SunnyAg", SunnyAg(context)) context.define_subparsers("main", { 'withdraw': None, }) psr = context.get_parser('main.withdraw') psr.add_argument("amount", type=int, help="Amount to withdraw") psr.add_argument("-p", "--pool", dest="pool_name", type=str, help="Pool name: can be 'pSOL_prtSOL' or 'xFTT_wFTT' for now") comp.run() * A few changes in the code above: * I'm now retrieving my private key from an environment variable as I don't want to have to copy/paste each time. * I'm providing a "pool name" on the command line, and use that to select the correct lists of accounts that should be used to configure the instructions. * I also fixed the part waiting for the confirmation/finalization of the transaction 😉 * And with that, I can withdraw some Lps from the xFTT/wFTT pool for instance: $ nvp sunnyag withdraw 1000000 -p xFTT_wFTT 2022/05/20 07:26:07 [__main__] INFO: Trying to withdraw funds... 2022/05/20 07:26:07 [__main__] INFO: My public key is: 7X2mFEFeddSsK2WMCP3rqv7fzLPs6MLCSG1GWEmGzfDn 2022/05/20 07:26:07 [__main__] INFO: Using amount representation: 40420f0000000000 2022/05/20 07:26:07 [__main__] INFO: Pool name: xFTT_wFTT 2022/05/20 07:26:07 [__main__] INFO: Sending transaction... 2022/05/20 07:26:08 [__main__] INFO: Got RPC response: {'jsonrpc': '2.0', 'result': 'eAbeV3TLihP4zzbBeTCen6pkUUz7Dqd8BDk7NswaGjQPLyDkrLVFb3n4teyV5jrKK7d4bmAN5KBkgFfhxwwVC23', 'id': 3} 2022/05/20 07:26:24 [__main__] INFO: Transaction status: { 'confirmationStatus': 'confirmed', 'confirmations': 10, 'err': None, 'slot': 134493920, 'status': {'Ok': None}} 2022/05/20 07:26:24 [__main__] INFO: Waiting for confirmation... 2022/05/20 07:26:54 [__main__] INFO: Transaction eAbeV3TLihP4zzbBeTCen6pkUUz7Dqd8BDk7NswaGjQPLyDkrLVFb3n4teyV5jrKK7d4bmAN5KBkgFfhxwwVC23 is finalized! * Now I just need to add more pool accounts in the **ALL_ACCOUNTS** dict as needed 👍! ===== Adding the xETH_whETH pool ===== * As a test I simply added this pool in the accounts list: "xETH_whETH": [ [ "LDdq4KhNRuGUjno4waam4HTkyftYpeRjC6Rbjtiao8b", "6FUf8Svq5Go5Gj4mF4PUq6Y2m8ATYM8vC3pHKZ9oLVC", "BYaEPQ4aWSMkBtuzYycowy4c7C9jCnzALcZtD1fsRsj4", "gZWtNxBX8XgPoYXbYp2UUNpJeuEyzoTxKUrotYZ4uiY", "!97PmYbGpSHSrKrUkQX793mjpA2EA9rrQKkHsQuvenU44", "G1ZpzaBpqNt7hpbRnc6vbiA6fvt2wAoESpnoEM2uFKyk", "EQJBn6WZ9AQfZMhcn9jBWJAS9WpzqjGtTJcMSqGtwkvc", "4pdW8H4DpZ9zBtyE95fUeFvqCKACMkiA8obJeWHr39J7", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "!QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", ], [ "3ibyFGq9oZsAcM6c3CqRX6SoPCukU7bCcAFPq92XmLyT", "BYaEPQ4aWSMkBtuzYycowy4c7C9jCnzALcZtD1fsRsj4", "gZWtNxBX8XgPoYXbYp2UUNpJeuEyzoTxKUrotYZ4uiY", "!rXhAofQCT7NN9TUqigyEAUzV1uLL4boeD8CRkNBSkYk", "CipQjauQwCa2pBbavhtR67mKUHbd8ymrQsDwJHTxjPFB", "3vfAj1i6xWn3RzmyBcqMap7D44zs7Bbpqet4yuDoUhmn", "DbYKjt7QLcbrxQ6JEVfsVPBQfTsFDgbSLAbW8AWSPWmp", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "!QMNeHCGYnLVDn1icRAfQZpjPLBNkfGbSKRB83G5d8KB", ], [ "BYaEPQ4aWSMkBtuzYycowy4c7C9jCnzALcZtD1fsRsj4", "gZWtNxBX8XgPoYXbYp2UUNpJeuEyzoTxKUrotYZ4uiY", "3ibyFGq9oZsAcM6c3CqRX6SoPCukU7bCcAFPq92XmLyT", "BDLQPQcTf9cy1S7ge4vbwmwNPjd3czaeFcbFcM8caLMH", "CzxdRhX3gEFZqABWksonUGCURCtNBntAmQAaWiYgVLWE", "!TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", ] ] * And then try to withdraw some LPs: $ nvp sunnyag withdraw 1000000 -p xETH_whETH 2022/05/20 07:39:33 [__main__] INFO: Trying to withdraw funds... 2022/05/20 07:39:33 [__main__] INFO: My public key is: 7X2mFEFeddSsK2WMCP3rqv7fzLPs6MLCSG1GWEmGzfDn 2022/05/20 07:39:34 [__main__] INFO: Using amount representation: 40420f0000000000 2022/05/20 07:39:34 [__main__] INFO: Pool name: xETH_whETH 2022/05/20 07:39:34 [__main__] INFO: Sending transaction... 2022/05/20 07:39:34 [__main__] INFO: Got RPC response: {'jsonrpc': '2.0', 'result': '5rh42h8onFJLMVzhimtFd5hFDALyCF2JQSqzVMw57DtCjafno8vQjgumf7KMGJzLM2LGbnvdy3FRAJeawBFeQH8J', 'id': 3} 2022/05/20 07:39:50 [__main__] INFO: Transaction status: { 'confirmationStatus': 'confirmed', 'confirmations': 0, 'err': None, 'slot': 134495101, 'status': {'Ok': None}} 2022/05/20 07:39:50 [__main__] INFO: Waiting for confirmation... 2022/05/20 07:40:20 [__main__] INFO: Transaction status: { 'confirmationStatus': 'confirmed', 'confirmations': 30, 'err': None, 'slot': 134495101, 'status': {'Ok': None}} 2022/05/20 07:40:20 [__main__] INFO: Waiting for confirmation... 2022/05/20 07:40:50 [__main__] INFO: Transaction 5rh42h8onFJLMVzhimtFd5hFDALyCF2JQSqzVMw57DtCjafno8vQjgumf7KMGJzLM2LGbnvdy3FRAJeawBFeQH8J is finalized! * => **Alright**! That's good enough for me! Now I can withdraw my LPs from sunnyag on my own, and I did learn a few things on solana programming in the process, that was nice 😉! I hope this quick project will help some people retrieving their funds when they really want to! And in case you appreciated this article, feel free to send me to tip to by me a beer or too [I like beers... ðŸĪĢ] at my solana address: **7X2mFEFeddSsK2WMCP3rqv7fzLPs6MLCSG1GWEmGzfDn**