Solana study case: Withdrawing LPs from SunnyAG protocol

Okay guys, so, that's not quite what I wanted to handle next, but I currently have a bit of a problem with the 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 😊!

  • 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.
  • 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:
  • 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:
    • 17f74c1e96ead91e00863ba101000000
    • 17f74c1e96ead91e0070929b02000000
  • 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.
  • As usual I'm building this as a minimal component using my 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.
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 <module>
        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 <module>
        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 here on solscan.
  • 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 <module>
        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 <module>
        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.
  • 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 👍!
  • 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
  • blog/2022/0520_crypto_withdraw_from_sunnyag.txt
  • Last modified: 2022/05/20 07:54
  • by 127.0.0.1