From cd7cdab540d371efba3f8e6008fef1f4d3da84d6 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 2 Jun 2021 20:03:46 +0100 Subject: [PATCH] Added commands to wrap and unwrap SOL. --- BaseModel.ipynb | 130 ++++++++++++++++---------------- bin/close-wrapped-sol-account | 88 ++++++++++++++++++++++ bin/send-sols | 4 +- bin/show-wrapped-sol | 62 +++++++++++++++ bin/unwrap-sol | 132 ++++++++++++++++++++++++++++++++ bin/wrap-sol | 138 ++++++++++++++++++++++++++++++++++ 6 files changed, 487 insertions(+), 67 deletions(-) create mode 100755 bin/close-wrapped-sol-account create mode 100755 bin/show-wrapped-sol create mode 100755 bin/unwrap-sol create mode 100755 bin/wrap-sol diff --git a/BaseModel.ipynb b/BaseModel.ipynb index 94490c0..f958dad 100644 --- a/BaseModel.ipynb +++ b/BaseModel.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "biological-pontiac", + "id": "interesting-continuity", "metadata": {}, "source": [ "# ⚠ Warning\n", @@ -16,7 +16,7 @@ }, { "cell_type": "markdown", - "id": "moral-enemy", + "id": "chemical-paper", "metadata": {}, "source": [ "# 🥭 BaseModel\n", @@ -33,7 +33,7 @@ { "cell_type": "code", "execution_count": null, - "id": "reverse-trade", + "id": "herbal-bracket", "metadata": { "jupyter": { "source_hidden": true @@ -69,7 +69,7 @@ }, { "cell_type": "markdown", - "id": "forty-booth", + "id": "aboriginal-active", "metadata": {}, "source": [ "## Version enum\n", @@ -80,7 +80,7 @@ { "cell_type": "code", "execution_count": null, - "id": "honest-development", + "id": "stretch-analyst", "metadata": {}, "outputs": [], "source": [ @@ -95,7 +95,7 @@ }, { "cell_type": "markdown", - "id": "western-cookbook", + "id": "cooked-india", "metadata": {}, "source": [ "## InstructionType enum\n", @@ -106,7 +106,7 @@ { "cell_type": "code", "execution_count": null, - "id": "reverse-patch", + "id": "composite-scotland", "metadata": {}, "outputs": [], "source": [ @@ -135,7 +135,7 @@ }, { "cell_type": "markdown", - "id": "presidential-glass", + "id": "rubber-wisconsin", "metadata": {}, "source": [ "## Internal functions\n", @@ -146,7 +146,7 @@ { "cell_type": "code", "execution_count": null, - "id": "recreational-success", + "id": "certain-particular", "metadata": {}, "outputs": [], "source": [ @@ -162,7 +162,7 @@ }, { "cell_type": "markdown", - "id": "pacific-medline", + "id": "innovative-conditioning", "metadata": {}, "source": [ "## AccountInfo class\n" @@ -171,7 +171,7 @@ { "cell_type": "code", "execution_count": null, - "id": "framed-miller", + "id": "beneficial-weekly", "metadata": {}, "outputs": [], "source": [ @@ -243,7 +243,7 @@ }, { "cell_type": "markdown", - "id": "developing-immune", + "id": "planned-referral", "metadata": {}, "source": [ "## AddressableAccount class\n", @@ -256,7 +256,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ruled-concern", + "id": "expired-stopping", "metadata": {}, "outputs": [], "source": [ @@ -275,7 +275,7 @@ }, { "cell_type": "markdown", - "id": "convinced-princeton", + "id": "unsigned-circus", "metadata": {}, "source": [ "## SerumAccountFlags class\n", @@ -286,7 +286,7 @@ { "cell_type": "code", "execution_count": null, - "id": "handmade-browser", + "id": "spare-large", "metadata": {}, "outputs": [], "source": [ @@ -329,7 +329,7 @@ }, { "cell_type": "markdown", - "id": "dramatic-armor", + "id": "substantial-worcester", "metadata": {}, "source": [ "## MangoAccountFlags class\n", @@ -340,7 +340,7 @@ { "cell_type": "code", "execution_count": null, - "id": "thorough-helicopter", + "id": "integral-uniform", "metadata": {}, "outputs": [], "source": [ @@ -373,7 +373,7 @@ }, { "cell_type": "markdown", - "id": "empty-sheffield", + "id": "tired-collins", "metadata": {}, "source": [ "## Index class" @@ -382,7 +382,7 @@ { "cell_type": "code", "execution_count": null, - "id": "partial-decimal", + "id": "designed-syndicate", "metadata": {}, "outputs": [], "source": [ @@ -409,7 +409,7 @@ }, { "cell_type": "markdown", - "id": "optimum-milton", + "id": "democratic-thread", "metadata": {}, "source": [ "## AggregatorConfig class" @@ -418,7 +418,7 @@ { "cell_type": "code", "execution_count": null, - "id": "parallel-absence", + "id": "coordinate-headquarters", "metadata": {}, "outputs": [], "source": [ @@ -451,7 +451,7 @@ }, { "cell_type": "markdown", - "id": "increased-leone", + "id": "therapeutic-audit", "metadata": {}, "source": [ "## Round class" @@ -460,7 +460,7 @@ { "cell_type": "code", "execution_count": null, - "id": "compatible-toronto", + "id": "liquid-solution", "metadata": {}, "outputs": [], "source": [ @@ -485,7 +485,7 @@ }, { "cell_type": "markdown", - "id": "improved-promise", + "id": "rural-candidate", "metadata": {}, "source": [ "## Answer class" @@ -494,7 +494,7 @@ { "cell_type": "code", "execution_count": null, - "id": "dying-insurance", + "id": "graphic-release", "metadata": {}, "outputs": [], "source": [ @@ -520,7 +520,7 @@ }, { "cell_type": "markdown", - "id": "straight-purse", + "id": "established-exemption", "metadata": {}, "source": [ "## Aggregator class" @@ -529,7 +529,7 @@ { "cell_type": "code", "execution_count": null, - "id": "conceptual-harvest", + "id": "refined-alexander", "metadata": {}, "outputs": [], "source": [ @@ -595,7 +595,7 @@ }, { "cell_type": "markdown", - "id": "medical-rogers", + "id": "varied-batch", "metadata": {}, "source": [ "## Token class\n", @@ -606,7 +606,7 @@ { "cell_type": "code", "execution_count": null, - "id": "solar-chambers", + "id": "three-legislature", "metadata": {}, "outputs": [], "source": [ @@ -661,7 +661,7 @@ }, { "cell_type": "markdown", - "id": "higher-carbon", + "id": "optimum-packaging", "metadata": {}, "source": [ "## SolToken object\n", @@ -672,7 +672,7 @@ { "cell_type": "code", "execution_count": null, - "id": "motivated-suite", + "id": "resistant-lawsuit", "metadata": {}, "outputs": [], "source": [ @@ -681,7 +681,7 @@ }, { "cell_type": "markdown", - "id": "prime-accused", + "id": "dimensional-interference", "metadata": {}, "source": [ "## TokenLookup class\n", @@ -703,7 +703,7 @@ { "cell_type": "code", "execution_count": null, - "id": "careful-thanksgiving", + "id": "attached-stocks", "metadata": {}, "outputs": [], "source": [ @@ -743,7 +743,7 @@ }, { "cell_type": "markdown", - "id": "intelligent-harmony", + "id": "informative-fortune", "metadata": {}, "source": [ "## SpotMarket class" @@ -752,7 +752,7 @@ { "cell_type": "code", "execution_count": null, - "id": "attempted-prayer", + "id": "rational-drain", "metadata": {}, "outputs": [], "source": [ @@ -776,7 +776,7 @@ }, { "cell_type": "markdown", - "id": "abandoned-network", + "id": "cordless-diversity", "metadata": {}, "source": [ "## SpotMarketLookup class\n", @@ -801,7 +801,7 @@ { "cell_type": "code", "execution_count": null, - "id": "grateful-child", + "id": "civic-sunset", "metadata": {}, "outputs": [], "source": [ @@ -883,7 +883,7 @@ }, { "cell_type": "markdown", - "id": "affecting-orlando", + "id": "satellite-shadow", "metadata": {}, "source": [ "## BasketToken class\n", @@ -894,7 +894,7 @@ { "cell_type": "code", "execution_count": null, - "id": "consistent-senator", + "id": "breathing-drove", "metadata": {}, "outputs": [], "source": [ @@ -950,7 +950,7 @@ }, { "cell_type": "markdown", - "id": "swiss-opinion", + "id": "centered-locking", "metadata": {}, "source": [ "## TokenValue class\n", @@ -961,7 +961,7 @@ { "cell_type": "code", "execution_count": null, - "id": "strange-pierre", + "id": "limited-wrapping", "metadata": {}, "outputs": [], "source": [ @@ -1046,7 +1046,7 @@ }, { "cell_type": "markdown", - "id": "forty-quebec", + "id": "metallic-director", "metadata": {}, "source": [ "## OwnedTokenValue class\n", @@ -1057,7 +1057,7 @@ { "cell_type": "code", "execution_count": null, - "id": "lovely-association", + "id": "honey-longitude", "metadata": {}, "outputs": [], "source": [ @@ -1097,7 +1097,7 @@ }, { "cell_type": "markdown", - "id": "ceramic-walker", + "id": "capable-sauce", "metadata": {}, "source": [ "## MarketMetadata class" @@ -1106,7 +1106,7 @@ { "cell_type": "code", "execution_count": null, - "id": "exact-writer", + "id": "continent-bookmark", "metadata": {}, "outputs": [], "source": [ @@ -1144,7 +1144,7 @@ }, { "cell_type": "markdown", - "id": "intellectual-biotechnology", + "id": "elect-principal", "metadata": {}, "source": [ "## Group class\n", @@ -1155,7 +1155,7 @@ { "cell_type": "code", "execution_count": null, - "id": "adverse-crisis", + "id": "premier-vaccine", "metadata": {}, "outputs": [], "source": [ @@ -1414,7 +1414,7 @@ }, { "cell_type": "markdown", - "id": "opposed-front", + "id": "rotary-therapist", "metadata": {}, "source": [ "## TokenAccount class" @@ -1423,7 +1423,7 @@ { "cell_type": "code", "execution_count": null, - "id": "clean-laundry", + "id": "little-portfolio", "metadata": {}, "outputs": [], "source": [ @@ -1502,12 +1502,12 @@ " return TokenAccount.parse(account_info)\n", "\n", " def __str__(self) -> str:\n", - " return f\"« Token: Mint: {self.mint}, Owner: {self.owner}, Amount: {self.amount} »\"\n" + " return f\"« Token: Address: {self.address}, Mint: {self.mint}, Owner: {self.owner}, Amount: {self.amount} »\"\n" ] }, { "cell_type": "markdown", - "id": "blocked-sudan", + "id": "exact-emergency", "metadata": {}, "source": [ "## OpenOrders class\n" @@ -1516,7 +1516,7 @@ { "cell_type": "code", "execution_count": null, - "id": "taken-shock", + "id": "spanish-promise", "metadata": {}, "outputs": [], "source": [ @@ -1636,7 +1636,7 @@ }, { "cell_type": "markdown", - "id": "elder-retailer", + "id": "listed-error", "metadata": {}, "source": [ "## BalanceSheet class" @@ -1645,7 +1645,7 @@ { "cell_type": "code", "execution_count": null, - "id": "featured-bandwidth", + "id": "vulnerable-conjunction", "metadata": {}, "outputs": [], "source": [ @@ -1692,7 +1692,7 @@ }, { "cell_type": "markdown", - "id": "manual-bubble", + "id": "seeing-victorian", "metadata": {}, "source": [ "## MarginAccount class\n" @@ -1701,7 +1701,7 @@ { "cell_type": "code", "execution_count": null, - "id": "expected-winner", + "id": "sunset-chase", "metadata": {}, "outputs": [], "source": [ @@ -1952,7 +1952,7 @@ }, { "cell_type": "markdown", - "id": "pointed-coral", + "id": "extraordinary-mozambique", "metadata": {}, "source": [ "## MarginAccountMetadata class" @@ -1961,7 +1961,7 @@ { "cell_type": "code", "execution_count": null, - "id": "approved-giving", + "id": "connected-cattle", "metadata": {}, "outputs": [], "source": [ @@ -1987,7 +1987,7 @@ }, { "cell_type": "markdown", - "id": "fossil-reserve", + "id": "green-boxing", "metadata": {}, "source": [ "# Events" @@ -1995,7 +1995,7 @@ }, { "cell_type": "markdown", - "id": "blessed-chain", + "id": "biological-penny", "metadata": {}, "source": [ "## LiquidationEvent" @@ -2004,7 +2004,7 @@ { "cell_type": "code", "execution_count": null, - "id": "parallel-ethics", + "id": "affecting-hydrogen", "metadata": {}, "outputs": [], "source": [ @@ -2040,7 +2040,7 @@ }, { "cell_type": "markdown", - "id": "threatened-union", + "id": "geological-pantyhose", "metadata": {}, "source": [ "# ✅ Testing" @@ -2049,7 +2049,7 @@ { "cell_type": "code", "execution_count": null, - "id": "internal-thong", + "id": "saving-lending", "metadata": {}, "outputs": [], "source": [ @@ -2128,7 +2128,7 @@ }, { "cell_type": "markdown", - "id": "virgin-waterproof", + "id": "insured-tackle", "metadata": {}, "source": [ "# 🏃 Running" @@ -2137,7 +2137,7 @@ { "cell_type": "code", "execution_count": null, - "id": "color-ultimate", + "id": "identified-margin", "metadata": {}, "outputs": [], "source": [ diff --git a/bin/close-wrapped-sol-account b/bin/close-wrapped-sol-account new file mode 100755 index 0000000..27e7382 --- /dev/null +++ b/bin/close-wrapped-sol-account @@ -0,0 +1,88 @@ +#!/usr/bin/env pyston3 + +import os +import sys + +from pathlib import Path + +# Get the full path to this script. +script_path = Path(os.path.realpath(__file__)) + +# The parent of the script is the bin directory. +# The parent of the bin directory is the notebook directory. +# It's this notebook directory we want. +notebook_directory = script_path.parent.parent + +# Add the notebook directory to our import path. +sys.path.append(str(notebook_directory)) + +# Add the startup directory to our import path. +startup_directory = notebook_directory / "meta" / "startup" +sys.path.append(str(startup_directory)) + +import argparse +import logging +import projectsetup # noqa: F401 +import typing + +from solana.account import Account +from solana.publickey import PublicKey +from spl.token.constants import TOKEN_PROGRAM_ID +from solana.transaction import Transaction +from spl.token.instructions import CloseAccountParams, close_account + +from BaseModel import TokenAccount, TokenLookup +from Constants import WARNING_DISCLAIMER_TEXT +from Context import default_context as context +from Wallet import Wallet + +parser = argparse.ArgumentParser(description='Creates a new wallet and private key, and saves it to a file.') +parser.add_argument("--id-file", type=str, default="id.json", + help="file containing the JSON-formatted wallet private key") +parser.add_argument("--address", type=PublicKey, + help="Public key of the Wrapped SOL account to close") +parser.add_argument("--log-level", default=logging.WARNING, type=lambda level: getattr(logging, level), + help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)") +parser.add_argument("--overwrite", action="store_true", default=False, + help="overwrite the ID file, if it exists (use with care!)") +args = parser.parse_args() + +logging.getLogger().setLevel(args.log_level) +logging.warning(WARNING_DISCLAIMER_TEXT) + +id_filename = args.id_file +if not os.path.isfile(id_filename): + logging.error(f"Wallet file '{id_filename}' is not present.") + sys.exit(1) +wallet = Wallet.load(id_filename) + +lookups = TokenLookup.default_lookups() +wrapped_sol = lookups.find_by_symbol("SOL") + +token_account = TokenAccount.load(context, args.address) +if (token_account is None) or (token_account.mint != wrapped_sol.mint): + raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.") + +transaction = Transaction() +signers: typing.List[Account] = [wallet.account] +payer = wallet.address + +transaction.add( + close_account( + CloseAccountParams( + account=args.address, + owner=wallet.address, + dest=wallet.address, + program_id=TOKEN_PROGRAM_ID, + ) + ) +) + +print(f"Closing account: {args.address} with balance {token_account.amount} lamports.") + +response = context.client.send_transaction(transaction, *signers) +transaction_id = context.unwrap_transaction_id_or_raise_exception(response) +print(f"Waiting on transaction ID: {transaction_id}") + +context.wait_for_confirmation(transaction_id) +print("Done.") diff --git a/bin/send-sols b/bin/send-sols index e53c27b..89881d0 100755 --- a/bin/send-sols +++ b/bin/send-sols @@ -31,7 +31,7 @@ from solana.publickey import PublicKey from solana.system_program import TransferParams, transfer from solana.transaction import Transaction -from Constants import WARNING_DISCLAIMER_TEXT +from Constants import SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id from Wallet import Wallet @@ -81,7 +81,7 @@ try: print(f"Balance: {sol_balance} SOL") # "A lamport has a value of 0.000000001 SOL." from https://docs.solana.com/introduction - lamports = int(args.quantity * Decimal(10 ** 9)) + lamports = int(args.quantity * SOL_DECIMAL_DIVISOR) source = wallet.address destination = args.address diff --git a/bin/show-wrapped-sol b/bin/show-wrapped-sol new file mode 100755 index 0000000..544ab5a --- /dev/null +++ b/bin/show-wrapped-sol @@ -0,0 +1,62 @@ +#!/usr/bin/env pyston3 + +import os +import sys + +from pathlib import Path + +# Get the full path to this script. +script_path = Path(os.path.realpath(__file__)) + +# The parent of the script is the bin directory. +# The parent of the bin directory is the notebook directory. +# It's this notebook directory we want. +notebook_directory = script_path.parent.parent + +# Add the notebook directory to our import path. +sys.path.append(str(notebook_directory)) + +# Add the startup directory to our import path. +startup_directory = notebook_directory / "meta" / "startup" +sys.path.append(str(startup_directory)) + +import argparse +import logging +import projectsetup # noqa: F401 + +from BaseModel import TokenAccount, TokenLookup +from Constants import WARNING_DISCLAIMER_TEXT +from Context import default_context as context +from Wallet import Wallet + +parser = argparse.ArgumentParser(description='Creates a new wallet and private key, and saves it to a file.') +parser.add_argument("--id-file", type=str, default="id.json", + help="file containing the JSON-formatted wallet private key") +parser.add_argument("--log-level", default=logging.WARNING, type=lambda level: getattr(logging, level), + help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)") +parser.add_argument("--overwrite", action="store_true", default=False, + help="overwrite the ID file, if it exists (use with care!)") +args = parser.parse_args() + +logging.getLogger().setLevel(args.log_level) +logging.warning(WARNING_DISCLAIMER_TEXT) + +id_filename = args.id_file +if not os.path.isfile(id_filename): + logging.error(f"Wallet file '{id_filename}' is not present.") + sys.exit(1) +wallet = Wallet.load(id_filename) + + +lookups = TokenLookup.default_lookups() +wrapped_sol = lookups.find_by_symbol("SOL") + +token_accounts = TokenAccount.fetch_all_for_owner_and_token(context, wallet.address, wrapped_sol) + +if len(token_accounts) == 0: + print("No wrapped SOL accounts") +else: + print(f"{wrapped_sol.name}:") + for account in token_accounts: + print(f" {account.address}: {account.amount:>18,.8f} {wrapped_sol.symbol}") + diff --git a/bin/unwrap-sol b/bin/unwrap-sol new file mode 100755 index 0000000..11903cd --- /dev/null +++ b/bin/unwrap-sol @@ -0,0 +1,132 @@ +#!/usr/bin/env pyston3 + +import os +import sys + +from pathlib import Path + +# Get the full path to this script. +script_path = Path(os.path.realpath(__file__)) + +# The parent of the script is the bin directory. +# The parent of the bin directory is the notebook directory. +# It's this notebook directory we want. +notebook_directory = script_path.parent.parent + +# Add the notebook directory to our import path. +sys.path.append(str(notebook_directory)) + +# Add the startup directory to our import path. +startup_directory = notebook_directory / "meta" / "startup" +sys.path.append(str(startup_directory)) + +import argparse +import logging +import projectsetup # noqa: F401 +import typing + +from decimal import Decimal +from solana.account import Account +from solana.system_program import CreateAccountParams, create_account +from solana.transaction import Transaction +from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT +from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2 + +from BaseModel import TokenAccount, TokenLookup +from Constants import SOL_DECIMAL_DIVISOR, SOL_DECIMALS, WARNING_DISCLAIMER_TEXT +from Context import default_context as context +from Wallet import Wallet + +parser = argparse.ArgumentParser(description='Unwraps Wrapped SOL to Pure SOL and adds it to the wallet account.') +parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of SOL to unwrap") +parser.add_argument("--id-file", type=str, default="id.json", + help="file containing the JSON-formatted wallet private key") +parser.add_argument("--log-level", default=logging.WARNING, type=lambda level: getattr(logging, level), + help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)") +parser.add_argument("--overwrite", action="store_true", default=False, + help="overwrite the ID file, if it exists (use with care!)") +args = parser.parse_args() + +logging.getLogger().setLevel(args.log_level) +logging.warning(WARNING_DISCLAIMER_TEXT) + +id_filename = args.id_file +if not os.path.isfile(id_filename): + logging.error(f"Wallet file '{id_filename}' is not present.") + sys.exit(1) +wallet = Wallet.load(id_filename) + + +lookups = TokenLookup.default_lookups() +wrapped_sol = lookups.find_by_symbol("SOL") + +largest_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, wrapped_sol) +if largest_token_account is None: + raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.") + +# Overpay - remainder should be sent back to our wallet. +FEE = Decimal(".005") +lamports_to_transfer = int((args.quantity + FEE) * SOL_DECIMAL_DIVISOR) + +transaction = Transaction() +signers: typing.List[Account] = [wallet.account] + +wrapped_sol_account = Account() +signers.append(wrapped_sol_account) + +transaction.add( + create_account( + CreateAccountParams( + from_pubkey=wallet.address, + new_account_pubkey=wrapped_sol_account.public_key(), + lamports=int(FEE * SOL_DECIMAL_DIVISOR), + space=ACCOUNT_LEN, + program_id=TOKEN_PROGRAM_ID, + ) + ) +) +transaction.add( + initialize_account( + InitializeAccountParams( + account=wrapped_sol_account.public_key(), + mint=WRAPPED_SOL_MINT, + owner=wallet.address, + program_id=TOKEN_PROGRAM_ID, + ) + ) +) +transaction.add( + transfer2( + Transfer2Params( + amount=int(args.quantity * SOL_DECIMAL_DIVISOR), + decimals=int(SOL_DECIMALS), + dest=wrapped_sol_account.public_key(), + mint=WRAPPED_SOL_MINT, + owner=wallet.address, + program_id=TOKEN_PROGRAM_ID, + source=largest_token_account.address + ) + ) +) +transaction.add( + close_account( + CloseAccountParams( + account=wrapped_sol_account.public_key(), + owner=wallet.address, + dest=wallet.address, + program_id=TOKEN_PROGRAM_ID, + ) + ) +) + +print("Unwrapping SOL:") +print(f" Temporary account: {wrapped_sol_account.public_key()}") +print(f" Source: {largest_token_account.address}") +print(f" Destination: {wallet.address}") + +response = context.client.send_transaction(transaction, *signers) +transaction_id = context.unwrap_transaction_id_or_raise_exception(response) +print(f"Waiting on transaction ID: {transaction_id}") + +context.wait_for_confirmation(transaction_id) +print("Transaction confirmed.") diff --git a/bin/wrap-sol b/bin/wrap-sol new file mode 100755 index 0000000..492bb50 --- /dev/null +++ b/bin/wrap-sol @@ -0,0 +1,138 @@ +#!/usr/bin/env pyston3 + +import os +import sys + +from pathlib import Path + +# Get the full path to this script. +script_path = Path(os.path.realpath(__file__)) + +# The parent of the script is the bin directory. +# The parent of the bin directory is the notebook directory. +# It's this notebook directory we want. +notebook_directory = script_path.parent.parent + +# Add the notebook directory to our import path. +sys.path.append(str(notebook_directory)) + +# Add the startup directory to our import path. +startup_directory = notebook_directory / "meta" / "startup" +sys.path.append(str(startup_directory)) + +import argparse +import logging +import projectsetup # noqa: F401 +import typing + +from decimal import Decimal +from solana.account import Account +from solana.system_program import CreateAccountParams, create_account +from solana.transaction import Transaction +from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT +from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2 + +from BaseModel import TokenAccount, TokenLookup +from Constants import SOL_DECIMAL_DIVISOR, SOL_DECIMALS, WARNING_DISCLAIMER_TEXT +from Context import default_context as context +from Wallet import Wallet + +parser = argparse.ArgumentParser(description='Creates a new wallet and private key, and saves it to a file.') +parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of SOL to wrap") +parser.add_argument("--id-file", type=str, default="id.json", + help="file containing the JSON-formatted wallet private key") +parser.add_argument("--log-level", default=logging.WARNING, type=lambda level: getattr(logging, level), + help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)") +parser.add_argument("--overwrite", action="store_true", default=False, + help="overwrite the ID file, if it exists (use with care!)") +args = parser.parse_args() + +logging.getLogger().setLevel(args.log_level) +logging.warning(WARNING_DISCLAIMER_TEXT) + +id_filename = args.id_file +if not os.path.isfile(id_filename): + logging.error(f"Wallet file '{id_filename}' is not present.") + sys.exit(1) +wallet = Wallet.load(id_filename) + + +lookups = TokenLookup.default_lookups() +wrapped_sol = lookups.find_by_symbol("SOL") + +token_accounts = TokenAccount.fetch_all_for_owner_and_token(context, wallet.address, wrapped_sol) + +if len(token_accounts) == 0: + close_wrapping_account = False +else: + close_wrapping_account = True + +# Overpay - remainder should be sent back to our wallet. +FEE = Decimal(".005") +lamports_to_transfer = int((args.quantity + FEE) * SOL_DECIMAL_DIVISOR) + +transaction = Transaction() +signers: typing.List[Account] = [wallet.account] + +wrapped_sol_account = Account() +signers.append(wrapped_sol_account) + +transaction.add( + create_account( + CreateAccountParams( + from_pubkey=wallet.address, + new_account_pubkey=wrapped_sol_account.public_key(), + lamports=lamports_to_transfer, + space=ACCOUNT_LEN, + program_id=TOKEN_PROGRAM_ID, + ) + ) +) +transaction.add( + initialize_account( + InitializeAccountParams( + account=wrapped_sol_account.public_key(), + mint=WRAPPED_SOL_MINT, + owner=wallet.address, + program_id=TOKEN_PROGRAM_ID, + ) + ) +) + +print("Wrapping SOL:") +if len(token_accounts) == 0: + print(f" Source: {wallet.address}") + print(f" Destination: {wrapped_sol_account.public_key()}") +else: + print(f" Temporary account: {wrapped_sol_account.public_key()}") + print(f" Source: {wallet.address}") + print(f" Destination: {token_accounts[0].address}") + transaction.add( + transfer2( + Transfer2Params( + amount=int(args.quantity * SOL_DECIMAL_DIVISOR), + decimals=int(SOL_DECIMALS), + dest=token_accounts[0].address, + mint=WRAPPED_SOL_MINT, + owner=wallet.address, + program_id=TOKEN_PROGRAM_ID, + source=wrapped_sol_account.public_key() + ) + ) + ) + transaction.add( + close_account( + CloseAccountParams( + account=wrapped_sol_account.public_key(), + owner=wallet.address, + dest=wallet.address, + program_id=TOKEN_PROGRAM_ID, + ) + ) + ) + +response = context.client.send_transaction(transaction, *signers) +transaction_id = context.unwrap_transaction_id_or_raise_exception(response) +print(f"Waiting on transaction ID: {transaction_id}") +context.wait_for_confirmation(transaction_id) +print("Done.")