From 8369ac166ed90a7e6b07060178ed70745bd97bc3 Mon Sep 17 00:00:00 2001 From: Dana Date: Mon, 22 Nov 2021 19:26:16 -0500 Subject: [PATCH] implement token swap (#260) --- README.md | 1 + SharedBuildProperties.props | 2 +- src/Solnet.Examples/TokenSwapExample.cs | 340 +++++++++++++++ src/Solnet.Programs/InstructionDecoder.cs | 4 +- .../Models/DecodedInstruction.cs | 23 + src/Solnet.Programs/Solnet.Programs.csproj | 4 + .../TokenSwap/Models/ConstantProductCurve.cs | 21 + .../TokenSwap/Models/CurveCalculator.cs | 16 + .../TokenSwap/Models/CurveType.cs | 17 + src/Solnet.Programs/TokenSwap/Models/Fees.cs | 69 +++ .../TokenSwap/Models/SwapCurve.cs | 43 ++ .../TokenSwap/TokenSwapProgram.cs | 407 ++++++++++++++++++ .../TokenSwap/TokenSwapProgramData.cs | 286 ++++++++++++ .../TokenSwap/TokenSwapProgramInstructions.cs | 65 +++ .../TokenSwapProgramTest.cs | 385 +++++++++++++++++ 15 files changed, 1681 insertions(+), 2 deletions(-) create mode 100644 src/Solnet.Examples/TokenSwapExample.cs create mode 100644 src/Solnet.Programs/TokenSwap/Models/ConstantProductCurve.cs create mode 100644 src/Solnet.Programs/TokenSwap/Models/CurveCalculator.cs create mode 100644 src/Solnet.Programs/TokenSwap/Models/CurveType.cs create mode 100644 src/Solnet.Programs/TokenSwap/Models/Fees.cs create mode 100644 src/Solnet.Programs/TokenSwap/Models/SwapCurve.cs create mode 100644 src/Solnet.Programs/TokenSwap/TokenSwapProgram.cs create mode 100644 src/Solnet.Programs/TokenSwap/TokenSwapProgramData.cs create mode 100644 src/Solnet.Programs/TokenSwap/TokenSwapProgramInstructions.cs create mode 100644 test/Solnet.Programs.Test/TokenSwapProgramTest.cs diff --git a/README.md b/README.md index 4ba6f7d1..f63d9605 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Solnet is Solana's .NET SDK to integrate with the .NET ecosystem. Wherever you a - Solana Program Library (SPL) - Memo Program - Token Program + - Token Swap Program - Associated Token Account Program - Name Service Program - Shared Memory Program diff --git a/SharedBuildProperties.props b/SharedBuildProperties.props index c7ed7af0..6bd742ea 100644 --- a/SharedBuildProperties.props +++ b/SharedBuildProperties.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Solnet - 0.4.13 + 0.4.14 Copyright 2021 © Solnet blockmountain blockmountain diff --git a/src/Solnet.Examples/TokenSwapExample.cs b/src/Solnet.Examples/TokenSwapExample.cs new file mode 100644 index 00000000..542beebd --- /dev/null +++ b/src/Solnet.Examples/TokenSwapExample.cs @@ -0,0 +1,340 @@ +using Solnet.Programs; +using Solnet.Programs.TokenSwap; +using Solnet.Programs.TokenSwap.Models; +using Solnet.Rpc; +using Solnet.Rpc.Builders; +using Solnet.Rpc.Core.Http; +using Solnet.Rpc.Messages; +using Solnet.Rpc.Models; +using Solnet.Wallet; +using System; + +namespace Solnet.Examples +{ + public class TokenSwapExample : IExample + { + + private static readonly IRpcClient RpcClient = ClientFactory.GetClient(Cluster.TestNet); + + private const string MnemonicWords = + "route clerk disease box emerge airport loud waste attitude film army tray " + + "forward deal onion eight catalog surface unit card window walnut wealth medal"; + + public void Run() + { + Wallet.Wallet wallet = new Wallet.Wallet(MnemonicWords); + + var tokenAMint = new Account(); + var tokenAUserAccount = new Account(); + var tokenBMint = new Account(); + var tokenBUserAccount = new Account(); + + //setup some mints and tokens owned by wallet + RequestResult> blockHash = RpcClient.GetRecentBlockHash(); + var tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + tokenAMint, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.MintAccountDataSize).Result, + TokenProgram.MintAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + tokenBMint, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.MintAccountDataSize).Result, + TokenProgram.MintAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + tokenAUserAccount, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result, + TokenProgram.TokenAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + tokenBUserAccount, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result, + TokenProgram.TokenAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(TokenProgram.InitializeMint( + tokenAMint, + 9, + wallet.Account + )) + .AddInstruction(TokenProgram.InitializeMint( + tokenBMint, + 9, + wallet.Account + )) + .AddInstruction(TokenProgram.InitializeAccount( + tokenAUserAccount, + tokenAMint, + wallet.Account + )) + .AddInstruction(TokenProgram.InitializeAccount( + tokenBUserAccount, + tokenBMint, + wallet.Account + )) + .AddInstruction(TokenProgram.MintTo( + tokenAMint, + tokenAUserAccount, + 1_000_000_000_000, + wallet.Account + )) + .AddInstruction(TokenProgram.MintTo( + tokenBMint, + tokenBUserAccount, + 1_000_000_000_000, + wallet.Account + )) + .Build(new Account[] { wallet.Account, tokenAMint, tokenBMint, tokenAUserAccount, tokenBUserAccount }); + var txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + var swap = new Account(); + var program = new TokenSwapProgram(swap); + + var swapTokenAAccount= new Account(); + var swapTokenBAccount = new Account(); + + //init the swap authority's token accounts + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + swapTokenAAccount, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result, + TokenProgram.TokenAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(TokenProgram.InitializeAccount( + swapTokenAAccount, + tokenAMint, + program.SwapAuthority + )) + .AddInstruction(TokenProgram.Transfer( + tokenAUserAccount, + swapTokenAAccount, + 5_000_000_000, + wallet.Account + )) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + swapTokenBAccount, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result, + TokenProgram.TokenAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(TokenProgram.InitializeAccount( + swapTokenBAccount, + tokenBMint, + program.SwapAuthority + )) + .AddInstruction(TokenProgram.Transfer( + tokenBUserAccount, + swapTokenBAccount, + 5_000_000_000, + wallet.Account + )) + .Build(new Account[] { wallet.Account, swapTokenAAccount, swapTokenBAccount }); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + var poolMint = new Account(); + var poolUserAccount = new Account(); + var poolFeeAccount = new Account(); + + //create the pool mint and the user and fee pool token accounts + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + poolMint, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.MintAccountDataSize).Result, + TokenProgram.MintAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(TokenProgram.InitializeMint( + poolMint, + 9, + program.SwapAuthority + )) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + poolUserAccount, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result, + TokenProgram.TokenAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(TokenProgram.InitializeAccount( + poolUserAccount, + poolMint, + wallet.Account + )) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + poolFeeAccount, + RpcClient.GetMinimumBalanceForRentExemption(TokenProgram.TokenAccountDataSize).Result, + TokenProgram.TokenAccountDataSize, + TokenProgram.ProgramIdKey + )) + .AddInstruction(TokenProgram.InitializeAccount( + poolFeeAccount, + poolMint, + program.OwnerKey + )) + .Build(new Account[] { wallet.Account, poolMint, poolUserAccount, poolFeeAccount }); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + //create the swap + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(SystemProgram.CreateAccount( + wallet.Account, + swap, + RpcClient.GetMinimumBalanceForRentExemption((long)program.TokenSwapAccountDataSize).Result, + program.TokenSwapAccountDataSize, + program.ProgramIdKey + )) + .AddInstruction(program.Initialize( + swapTokenAAccount, + swapTokenBAccount, + poolMint, + poolFeeAccount, + poolUserAccount, + new Fees() + { + TradeFeeNumerator = 25, + TradeFeeDenominator = 10000, + OwnerTradeFeeNumerator = 5, + OwnerTradeFeeDenomerator = 10000, + OwnerWithrawFeeNumerator = 0, + OwnerWithrawFeeDenomerator = 0, + HostFeeNumerator = 20, + HostFeeDenomerator = 100 + }, + SwapCurve.ConstantProduct + )) + .Build(new Account[] { wallet.Account, swap }); + Console.WriteLine($"Swap Account: {swap}"); + Console.WriteLine($"Swap Auth Account: {program.SwapAuthority}"); + Console.WriteLine($"Pool Mint Account: {poolMint}"); + Console.WriteLine($"Pool User Account: {poolUserAccount}"); + Console.WriteLine($"Pool Fee Account: {poolFeeAccount}"); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + //now a user can swap in the pool + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(program.Swap( + wallet.Account, + tokenAUserAccount, + swapTokenAAccount, + swapTokenBAccount, + tokenBUserAccount, + poolMint, + poolFeeAccount, + null, + 1_000_000_000, + 500_000)) + .Build(wallet.Account); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + //user can add liq + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(program.DepositAllTokenTypes( + wallet.Account, + tokenAUserAccount, + tokenBUserAccount, + swapTokenAAccount, + swapTokenBAccount, + poolMint, + poolUserAccount, + 1_000_000, + 100_000_000_000, + 100_000_000_000)) + .Build(wallet.Account); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + //user can remove liq + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(program.WithdrawAllTokenTypes( + wallet.Account, + poolMint, + poolUserAccount, + swapTokenAAccount, + swapTokenBAccount, + tokenAUserAccount, + tokenBUserAccount, + poolFeeAccount, + 1_000_000, + 1_000, + 1_000)) + .Build(wallet.Account); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + //user can deposit single + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(program.DepositSingleTokenTypeExactAmountIn( + wallet.Account, + tokenAUserAccount, + swapTokenAAccount, + swapTokenBAccount, + poolMint, + poolUserAccount, + 1_000_000_000, + 1_000)) + .Build(wallet.Account); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + + //user can withdraw single + blockHash = RpcClient.GetRecentBlockHash(); + tx = new TransactionBuilder() + .SetRecentBlockHash(blockHash.Result.Value.Blockhash) + .SetFeePayer(wallet.Account) + .AddInstruction(program.WithdrawSingleTokenTypeExactAmountOut( + wallet.Account, + poolMint, + poolUserAccount, + swapTokenAAccount, + swapTokenBAccount, + tokenAUserAccount, + poolFeeAccount, + 1_000_000, + 100_000)) + .Build(wallet.Account); + txSig = Examples.SubmitTxSendAndLog(tx); + Examples.PollConfirmedTx(txSig); + } + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/InstructionDecoder.cs b/src/Solnet.Programs/InstructionDecoder.cs index 188598c5..cff82a9f 100644 --- a/src/Solnet.Programs/InstructionDecoder.cs +++ b/src/Solnet.Programs/InstructionDecoder.cs @@ -1,4 +1,5 @@ -using Solnet.Rpc.Builders; +using Solnet.Programs.TokenSwap; +using Solnet.Rpc.Builders; using Solnet.Rpc.Models; using Solnet.Wallet; using Solnet.Wallet.Utilities; @@ -32,6 +33,7 @@ static InstructionDecoder() InstructionDictionary.Add(MemoProgram.ProgramIdKeyV2, MemoProgram.Decode); InstructionDictionary.Add(SystemProgram.ProgramIdKey, SystemProgram.Decode); InstructionDictionary.Add(TokenProgram.ProgramIdKey, TokenProgram.Decode); + InstructionDictionary.Add(TokenSwapProgram.TokenSwapProgramIdKey, TokenSwapProgram.Decode); InstructionDictionary.Add(AssociatedTokenAccountProgram.ProgramIdKey, AssociatedTokenAccountProgram.Decode); InstructionDictionary.Add(NameServiceProgram.ProgramIdKey, NameServiceProgram.Decode); InstructionDictionary.Add(SharedMemoryProgram.ProgramIdKey, SharedMemoryProgram.Decode); diff --git a/src/Solnet.Programs/Models/DecodedInstruction.cs b/src/Solnet.Programs/Models/DecodedInstruction.cs index e6a56789..cd7769a7 100644 --- a/src/Solnet.Programs/Models/DecodedInstruction.cs +++ b/src/Solnet.Programs/Models/DecodedInstruction.cs @@ -1,5 +1,7 @@ using Solnet.Wallet; using System.Collections.Generic; +using System.Linq; +using System.Text; namespace Solnet.Programs { @@ -32,5 +34,26 @@ public class DecodedInstruction /// The inner instructions related to this decoded instruction. /// public List InnerInstructions { get; set; } + + /// + /// Converts the decoded instructions to a string + /// + /// A string representation of the decoded instructions + public override string ToString() => ToString(0); + + /// + /// Converts the decoded instructions to a string, indented a certain amount + /// + /// A string representation of the decoded instructions, indented a certain amount + public string ToString(int indent) + { + var sb = new StringBuilder(); + sb.Append($"{new string(Enumerable.Repeat(' ', indent * 4).ToArray())}[{indent}] {PublicKey}:{ProgramName}:{InstructionName}\n"); + sb.Append($"{new string(Enumerable.Repeat(' ', indent * 4).ToArray())}[{indent}] [{string.Join(',', Values.Select(a=>a))}]\n"); + sb.Append($"{new string(Enumerable.Repeat(' ', indent * 4).ToArray())}[{indent}] InnerInstructions ({InnerInstructions.Count})\n"); + foreach (var item in InnerInstructions) + sb.Append(item.ToString(indent + 1)); + return sb.ToString(); + } } } \ No newline at end of file diff --git a/src/Solnet.Programs/Solnet.Programs.csproj b/src/Solnet.Programs/Solnet.Programs.csproj index 6e65dab4..48760346 100644 --- a/src/Solnet.Programs/Solnet.Programs.csproj +++ b/src/Solnet.Programs/Solnet.Programs.csproj @@ -8,5 +8,9 @@ + + + + diff --git a/src/Solnet.Programs/TokenSwap/Models/ConstantProductCurve.cs b/src/Solnet.Programs/TokenSwap/Models/ConstantProductCurve.cs new file mode 100644 index 00000000..88b6579e --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/Models/ConstantProductCurve.cs @@ -0,0 +1,21 @@ +using System; + +namespace Solnet.Programs.TokenSwap.Models +{ + /// + /// Uniswap-style constant product curve, invariant = token_a_amount * token_b_amount + /// + public class ConstantProductCurve : CurveCalculator + { + + /// + /// Serialize the Fees + /// + /// Serialized Fees + public ReadOnlySpan Serialize() + { + return new Span(new byte[0]); + } + + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/TokenSwap/Models/CurveCalculator.cs b/src/Solnet.Programs/TokenSwap/Models/CurveCalculator.cs new file mode 100644 index 00000000..aad7dea6 --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/Models/CurveCalculator.cs @@ -0,0 +1,16 @@ +using System; + +namespace Solnet.Programs.TokenSwap.Models +{ + /// + /// A curve calculator must serialize itself to 32 bytes + /// + public interface CurveCalculator + { + /// + /// Serialize this calculator type + /// + /// + ReadOnlySpan Serialize(); + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/TokenSwap/Models/CurveType.cs b/src/Solnet.Programs/TokenSwap/Models/CurveType.cs new file mode 100644 index 00000000..5ac42ecb --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/Models/CurveType.cs @@ -0,0 +1,17 @@ +namespace Solnet.Programs.TokenSwap.Models +{ + /// + /// Curve type enum for an instruction + /// + public enum CurveType + { + /// Uniswap-style constant product curve, invariant = token_a_amount * token_b_amount + ConstantProduct = 0, + /// Flat line, always providing 1:1 from one token to another + ConstantPrice = 1, + /// Stable, like uniswap, but with wide zone of 1:1 instead of one point + Stable = 2, + /// Offset curve, like Uniswap, but the token B side has a faked offset + Offset = 3, + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/TokenSwap/Models/Fees.cs b/src/Solnet.Programs/TokenSwap/Models/Fees.cs new file mode 100644 index 00000000..91651287 --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/Models/Fees.cs @@ -0,0 +1,69 @@ +using System; +using Solnet.Programs.Utilities; + +namespace Solnet.Programs.TokenSwap.Models +{ + /// + /// Encapsulates all fee information and calculations for swap operations + /// + public class Fees + { + /// + /// Trade fee numerator. + /// + public ulong TradeFeeNumerator; + + /// + /// Trade fee denominator. + /// + public ulong TradeFeeDenominator; + + /// + /// Owner trade fee numerator. + /// + public ulong OwnerTradeFeeNumerator; + + /// + /// Owner trade fee denominator. + /// + public ulong OwnerTradeFeeDenomerator; + + /// + /// Owner withdraw fee numerator. + /// + public ulong OwnerWithrawFeeNumerator; + + /// + /// Owner withdraw fee denominator. + /// + public ulong OwnerWithrawFeeDenomerator; + + /// + /// Host trading fee numerator. + /// + public ulong HostFeeNumerator; + + /// + /// Host trading fee denominator. + /// + public ulong HostFeeDenomerator; + + /// + /// Serialize the Fees + /// + /// Serialized Fees + public Span Serialize() + { + var ret = new byte[64]; + ret.WriteU64(TradeFeeNumerator, 0); + ret.WriteU64(TradeFeeDenominator, 8); + ret.WriteU64(OwnerTradeFeeNumerator, 16); + ret.WriteU64(OwnerTradeFeeDenomerator, 24); + ret.WriteU64(OwnerWithrawFeeNumerator, 32); + ret.WriteU64(OwnerWithrawFeeDenomerator, 40); + ret.WriteU64(HostFeeNumerator, 48); + ret.WriteU64(HostFeeDenomerator, 56); + return new Span(ret); + } + } +} diff --git a/src/Solnet.Programs/TokenSwap/Models/SwapCurve.cs b/src/Solnet.Programs/TokenSwap/Models/SwapCurve.cs new file mode 100644 index 00000000..9504a996 --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/Models/SwapCurve.cs @@ -0,0 +1,43 @@ +using Solnet.Programs.Utilities; +using System; + +namespace Solnet.Programs.TokenSwap.Models +{ + /// + /// A swap curve type of a token swap. The static construction methods should be used to construct + /// + public class SwapCurve + { + /// + /// The curve type. + /// + public CurveType CurveType { get; set; } + + /// + /// The calculator used + /// + public CurveCalculator Calculator { get; set; } + + /// + /// Create a swap curve class. Protected as factory methods should be used to create + /// + protected SwapCurve() { } + + /// + /// Serialize this swap curve for an instruction + /// + /// + public virtual ReadOnlySpan Serialize() + { + var ret = new byte[33]; + ret.WriteU8((byte)CurveType, 0); + ret.WriteSpan(Calculator.Serialize(), 1); + return new Span(ret); + } + + /// + /// The constant procuct curve + /// + public static SwapCurve ConstantProduct => new SwapCurve() { CurveType = CurveType.ConstantProduct, Calculator = new ConstantProductCurve() }; + } +} diff --git a/src/Solnet.Programs/TokenSwap/TokenSwapProgram.cs b/src/Solnet.Programs/TokenSwap/TokenSwapProgram.cs new file mode 100644 index 00000000..d0ccb683 --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/TokenSwapProgram.cs @@ -0,0 +1,407 @@ +using Solnet.Programs.TokenSwap.Models; +using Solnet.Programs.Utilities; +using Solnet.Rpc.Models; +using Solnet.Rpc.Utilities; +using Solnet.Wallet; +using System; +using System.Collections.Generic; + +namespace Solnet.Programs.TokenSwap +{ + /// + /// Implements the Token Swap Program methods. + /// + /// For more information see: + /// https://spl.solana.com/token-swap + /// https://docs.rs/spl-token-swap/2.1.0/spl_token_swap/ + /// + /// + public class TokenSwapProgram + { + /// + /// SPL Token Swap Program Program ID + /// + public static readonly PublicKey TokenSwapProgramIdKey = new("SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8"); + + /// + /// SPL Token Swap Program Program Name + /// + public static readonly string TokenSwapProgramProgramName = "Token Swap Program"; + + /// + /// The public key of the Token Swap Program. + /// + public virtual PublicKey ProgramIdKey => TokenSwapProgramIdKey; + + /// + /// The program's name. + /// + public virtual string ProgramName => TokenSwapProgramProgramName; + + /// + /// The owner key required to use as the fee account owner. + /// + public virtual PublicKey OwnerKey => new("HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN"); + + /// + /// Token Swap account layout size. + /// + public virtual ulong TokenSwapAccountDataSize => 324; + + /// + /// Exposes the computed swap authority public key. Subclasses can override CreateAuthority to change the value. + /// + public PublicKey SwapAuthority => _swapAuthority; + + /// + /// Exposes the computed swap authority public key + /// + public byte SwapAuthorityNonce => _nonce; + + private readonly PublicKey _tokenSwapAccount; + private readonly PublicKey _swapAuthority; + private readonly byte _nonce; + + /// + /// Create a new instance to operation over the specified token swap + /// + /// The token swap that operations will be made over + public TokenSwapProgram(PublicKey tokenSwapAccount) + { + _tokenSwapAccount = tokenSwapAccount; + (_swapAuthority, _nonce) = CreateAuthority(); + } + + /// + /// Create the authority + /// + /// The swap authority + /// No program account could be found (exhausted nonces) + protected virtual (PublicKey, byte) CreateAuthority() + { + if (!AddressExtensions.TryFindProgramAddress(new[] { _tokenSwapAccount.KeyBytes }, ProgramIdKey.KeyBytes, out var addressBytes, out var nonce)) + throw new InvalidProgramException(); + var auth = new PublicKey(addressBytes); + return (auth, (byte)nonce); + } + + /// + /// Initializes a new swap. + /// + /// token_a Account. Must be non zero, owned by swap authority. + /// token_b Account. Must be non zero, owned by swap authority. + /// Pool Token Mint. Must be empty, owned by swap authority. + /// Pool Token Account to deposit trading and withdraw fees. Must be empty, not owned by swap authority. + /// Pool Token Account to deposit the initial pool token supply. Must be empty, not owned by swap authority. + /// Fees to use for this token swap. + /// Curve to use for this token swap. + /// The transaction instruction. + public virtual TransactionInstruction Initialize( + PublicKey tokenAAccount, + PublicKey tokenBAccount, + PublicKey poolTokenMint, + PublicKey poolTokenFeeAccount, + PublicKey userPoolTokenAccount, + Fees fees, SwapCurve swapCurve) + { + List keys = new() + { + AccountMeta.Writable(_tokenSwapAccount, true), + AccountMeta.ReadOnly(_swapAuthority, false), + AccountMeta.ReadOnly(tokenAAccount, false), + AccountMeta.ReadOnly(tokenBAccount, false), + AccountMeta.Writable(poolTokenMint, false), + AccountMeta.ReadOnly(poolTokenFeeAccount, false), + AccountMeta.Writable(userPoolTokenAccount, false), + AccountMeta.ReadOnly(TokenProgram.ProgramIdKey, false), + }; + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = TokenSwapProgramData.EncodeInitializeData(_nonce, fees, swapCurve) + }; + } + + /// + /// Swap the tokens in the pool. + /// + /// user transfer authority. + /// token_(A|B) SOURCE Account, amount is transferable by user transfer authority. + /// token_(A|B) Base Account to swap INTO. Must be the SOURCE token. + /// token_(A|B) Base Account to swap FROM. Must be the DESTINATION token. + /// token_(A|B) DESTINATION Account assigned to USER as the owner. + /// Pool token mint, to generate trading fees. + /// Fee account, to receive trading fees. + /// Host fee account to receive additional trading fees. + /// SOURCE amount to transfer, output to DESTINATION is based on the exchange rate. + /// Minimum amount of DESTINATION token to output, prevents excessive slippage. + /// The transaction instruction. + public virtual TransactionInstruction Swap( + PublicKey userTransferAuthority, + PublicKey tokenSourceAccount, + PublicKey tokenBaseIntoAccount, + PublicKey tokenBaseFromAccount, + PublicKey tokenDestinationAccount, + PublicKey poolTokenMint, + PublicKey poolTokenFeeAccount, + PublicKey poolTokenHostFeeAccount, + ulong amountIn, ulong amountOut) + { + List keys = new() + { + AccountMeta.ReadOnly(_tokenSwapAccount, false), + AccountMeta.ReadOnly(_swapAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, false), + AccountMeta.Writable(tokenSourceAccount, false), + AccountMeta.Writable(tokenBaseIntoAccount, false), + AccountMeta.Writable(tokenBaseFromAccount, false), + AccountMeta.Writable(tokenDestinationAccount, false), + AccountMeta.Writable(poolTokenMint, false), + AccountMeta.Writable(poolTokenFeeAccount, false), + AccountMeta.ReadOnly(TokenProgram.ProgramIdKey, false), + }; + if (poolTokenHostFeeAccount != null) + { + keys.Add(AccountMeta.Writable(poolTokenHostFeeAccount, false)); + } + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = TokenSwapProgramData.EncodeSwapData(amountIn, amountOut) + }; + } + + /// + /// Deposit both types of tokens into the pool. The output is a "pool" + /// token representing ownership in the pool. Inputs are converted to + /// the current ratio. + /// + /// user transfer authority. + /// token_a - user transfer authority can transfer amount. + /// token_b - user transfer authority can transfer amount. + /// token_a Base Account to deposit into. + /// token_b Base Account to deposit into. + /// Pool MINT account, swap authority is the owner. + /// Pool Account to deposit the generated tokens, user is the owner. + /// Pool token amount to transfer. token_a and token_b amount are set by the current exchange rate and size of the pool. + /// Maximum token A amount to deposit, prevents excessive slippage. + /// Maximum token B amount to deposit, prevents excessive slippage. + /// The transaction instruction. + public virtual TransactionInstruction DepositAllTokenTypes( + PublicKey userTransferAuthority, + PublicKey tokenAuserAccount, + PublicKey tokenBuserAccount, + PublicKey tokenADepositAccount, + PublicKey tokenBDepositAccount, + PublicKey poolTokenMint, + PublicKey poolTokenUserAccount, + ulong poolTokenAmount, ulong maxTokenA, ulong maxTokenB) + { + List keys = new() + { + AccountMeta.ReadOnly(_tokenSwapAccount, false), + AccountMeta.ReadOnly(_swapAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, false), + AccountMeta.Writable(tokenAuserAccount, false), + AccountMeta.Writable(tokenBuserAccount, false), + AccountMeta.Writable(tokenADepositAccount, false), + AccountMeta.Writable(tokenBDepositAccount, false), + AccountMeta.Writable(poolTokenMint, false), + AccountMeta.Writable(poolTokenUserAccount, false), + AccountMeta.ReadOnly(TokenProgram.ProgramIdKey, false), + }; + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = TokenSwapProgramData.EncodeDepositAllTokenTypesData(poolTokenAmount, maxTokenA, maxTokenB) + }; + } + + /// + /// Withdraw both types of tokens from the pool at the current ratio, given + /// pool tokens. The pool tokens are burned in exchange for an equivalent + /// amount of token A and B. + /// + /// user transfer authority. + /// Pool MINT account, swap authority is the owner. + /// SOURCE Pool account, amount is transferable by user transfer authority. + /// token_a Swap Account to withdraw FROM. + /// token_b Swap Account to withdraw FROM. + /// token_a user Account to credit. + /// token_b user Account to credit. + /// Fee account, to receive withdrawal fees. + /// Amount of pool tokens to burn. User receives an output of token a and b based on the percentage of the pool tokens that are returned. + /// Minimum amount of token A to receive, prevents excessive slippage. + /// Minimum amount of token B to receive, prevents excessive slippage. + /// The transaction instruction. + public virtual TransactionInstruction WithdrawAllTokenTypes( + PublicKey userTransferAuthority, + PublicKey poolTokenMint, + PublicKey sourcePoolAccount, + PublicKey tokenASwapAccount, + PublicKey tokenBSwapAccount, + PublicKey tokenAUserAccount, + PublicKey tokenBUserAccount, + PublicKey feeAccount, + ulong poolTokenAmount, ulong minTokenA, ulong minTokenB) + { + List keys = new() + { + AccountMeta.ReadOnly(_tokenSwapAccount, false), + AccountMeta.ReadOnly(_swapAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, false), + AccountMeta.Writable(poolTokenMint, false), + AccountMeta.Writable(sourcePoolAccount, false), + AccountMeta.Writable(tokenASwapAccount, false), + AccountMeta.Writable(tokenBSwapAccount, false), + AccountMeta.Writable(tokenAUserAccount, false), + AccountMeta.Writable(tokenBUserAccount, false), + AccountMeta.Writable(feeAccount, false), + AccountMeta.ReadOnly(TokenProgram.ProgramIdKey, false), + }; + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = TokenSwapProgramData.EncodeWithdrawAllTokenTypesData(poolTokenAmount, minTokenA, minTokenB) + }; + } + + /// + /// Deposit one type of tokens into the pool. The output is a "pool" token + /// representing ownership into the pool. Input token is converted as if + /// a swap and deposit all token types were performed. + /// + /// user transfer authority. + /// token_(A|B) SOURCE Account, amount is transferable by user transfer authority. + /// token_a Swap Account, may deposit INTO. + /// token_b Swap Account, may deposit INTO. + /// Pool MINT account, swap authority is the owner. + /// Pool Account to deposit the generated tokens, user is the owner. + /// Token amount to deposit. + /// Pool token amount to receive in exchange. The amount is set by the current exchange rate and size of the pool. + /// The transaction instruction. + public virtual TransactionInstruction DepositSingleTokenTypeExactAmountIn( + PublicKey userTransferAuthority, + PublicKey sourceAccount, + PublicKey destinationTokenAAccount, + PublicKey destinationTokenBAccount, + PublicKey poolMintAccount, + PublicKey poolTokenUserAccount, + ulong sourceTokenAmount, ulong minPoolTokenAmount) + { + List keys = new() + { + AccountMeta.ReadOnly(_tokenSwapAccount, false), + AccountMeta.ReadOnly(_swapAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, false), + AccountMeta.Writable(sourceAccount, false), + AccountMeta.Writable(destinationTokenAAccount, false), + AccountMeta.Writable(destinationTokenBAccount, false), + AccountMeta.Writable(poolMintAccount, false), + AccountMeta.Writable(poolTokenUserAccount, false), + AccountMeta.ReadOnly(TokenProgram.ProgramIdKey, false), + }; + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = TokenSwapProgramData.EncodeDepositSingleTokenTypeExactAmountInData(sourceTokenAmount, minPoolTokenAmount) + }; + } + + /// + /// Withdraw one token type from the pool at the current ratio given the + /// exact amount out expected. + /// + /// user transfer authority. + /// Pool mint account, swap authority is the owner. + /// SOURCE Pool account, amount is transferable by user transfer authority. + /// token_a Swap Account to potentially withdraw from. + /// token_b Swap Account to potentially withdraw from. + /// token_(A|B) User Account to credit. + /// Fee account, to receive withdrawal fees. + /// Amount of token A or B to receive. + /// Maximum amount of pool tokens to burn. User receives an output of token A or B based on the percentage of the pool tokens that are returned. + /// The transaction instruction. + public virtual TransactionInstruction WithdrawSingleTokenTypeExactAmountOut( + PublicKey userTransferAuthority, + PublicKey poolMintAccount, + PublicKey sourceUserAccount, + PublicKey tokenASwapAccount, + PublicKey tokenBSwapAccount, + PublicKey tokenUserAccount, + PublicKey feeAccount, + ulong destTokenAmount, ulong maxPoolTokenAmount) + { + List keys = new() + { + AccountMeta.ReadOnly(_tokenSwapAccount, false), + AccountMeta.ReadOnly(_swapAuthority, false), + AccountMeta.ReadOnly(userTransferAuthority, false), + AccountMeta.Writable(poolMintAccount, false), + AccountMeta.Writable(sourceUserAccount, false), + AccountMeta.Writable(tokenASwapAccount, false), + AccountMeta.Writable(tokenBSwapAccount, false), + AccountMeta.Writable(tokenUserAccount, false), + AccountMeta.Writable(feeAccount, false), + AccountMeta.ReadOnly(TokenProgram.ProgramIdKey, false), + }; + return new TransactionInstruction + { + ProgramId = ProgramIdKey.KeyBytes, + Keys = keys, + Data = TokenSwapProgramData.EncodeWithdrawSingleTokenTypeExactAmountOutData(destTokenAmount, maxPoolTokenAmount) + }; + } + + /// + /// Decodes an instruction created by the System Program. + /// + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + /// A decoded instruction. + public static DecodedInstruction Decode(ReadOnlySpan data, IList keys, byte[] keyIndices) + { + uint instruction = data.GetU8(TokenProgramData.MethodOffset); + TokenSwapProgramInstructions.Values instructionValue = + (TokenSwapProgramInstructions.Values)Enum.Parse(typeof(TokenSwapProgramInstructions.Values), instruction.ToString()); + + DecodedInstruction decodedInstruction = new() + { + PublicKey = TokenSwapProgram.TokenSwapProgramIdKey, + InstructionName = TokenSwapProgramInstructions.Names[instructionValue], + ProgramName = TokenSwapProgram.TokenSwapProgramProgramName, + Values = new Dictionary(), + InnerInstructions = new List() + }; + + switch (instructionValue) + { + case TokenSwapProgramInstructions.Values.Initialize: + TokenSwapProgramData.DecodeInitializeData(decodedInstruction, data, keys, keyIndices); + break; + case TokenSwapProgramInstructions.Values.Swap: + TokenSwapProgramData.DecodeSwapData(decodedInstruction, data, keys, keyIndices); + break; + case TokenSwapProgramInstructions.Values.DepositAllTokenTypes: + TokenSwapProgramData.DecodeDepositAllTokenTypesData(decodedInstruction, data, keys, keyIndices); + break; + case TokenSwapProgramInstructions.Values.WithdrawAllTokenTypes: + TokenSwapProgramData.DecodeWithdrawAllTokenTypesData(decodedInstruction, data, keys, keyIndices); + break; + case TokenSwapProgramInstructions.Values.DepositSingleTokenTypeExactAmountIn: + TokenSwapProgramData.DecodeDepositSingleTokenTypeExactAmountInData(decodedInstruction, data, keys, keyIndices); + break; + case TokenSwapProgramInstructions.Values.WithdrawSingleTokenTypeExactAmountOut: + TokenSwapProgramData.DecodeWithdrawSingleTokenTypeExactAmountOutData(decodedInstruction, data, keys, keyIndices); + break; + } + return decodedInstruction; + } + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/TokenSwap/TokenSwapProgramData.cs b/src/Solnet.Programs/TokenSwap/TokenSwapProgramData.cs new file mode 100644 index 00000000..1ee258ee --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/TokenSwapProgramData.cs @@ -0,0 +1,286 @@ +using Solnet.Programs.TokenSwap.Models; +using Solnet.Programs.Utilities; +using Solnet.Wallet; +using System; +using System.Collections.Generic; + +namespace Solnet.Programs.TokenSwap +{ + /// + /// Implements the token swap program data encodings. + /// + internal static class TokenSwapProgramData + { + /// + /// Encode the transaction instruction data for the method. + /// + /// nonce used to create valid program address. + /// all swap fees. + /// swap curve info for pool, including CurveType and anything else that may be required. + /// The freezeAuthorityOption parameter is related to the existence or not of a freeze authority. + /// The byte array with the encoded data. + internal static byte[] EncodeInitializeData(byte nonce, Fees fees, SwapCurve swapCurve) + { + byte[] methodBuffer = new byte[99]; + + methodBuffer.WriteU8((byte)TokenSwapProgramInstructions.Values.Initialize, 0); + methodBuffer.WriteU8(nonce, 1); + methodBuffer.WriteSpan(fees.Serialize(), 2); + methodBuffer.WriteSpan(swapCurve.Serialize(), 66); + + return methodBuffer; + } + + /// + /// Encode the transaction instruction data for the method. + /// + /// The amount of tokens in. + /// The amount of tokens out. + /// The byte array with the encoded data. + internal static byte[] EncodeSwapData(ulong amountIn, ulong amountOut) + { + byte[] methodBuffer = new byte[17]; + + methodBuffer.WriteU8((byte)TokenSwapProgramInstructions.Values.Swap, 0); + methodBuffer.WriteU64(amountIn, 1); + methodBuffer.WriteU64(amountOut, 9); + + return methodBuffer; + } + + /// + /// Encode the transaction instruction data for the method. + /// + /// The amount of tokens out. + /// The max amount of tokens A. + /// The max amount of tokens B. + /// The byte array with the encoded data. + internal static byte[] EncodeDepositAllTokenTypesData(ulong poolTokenAmount, ulong maxTokenAAmount, ulong maxTokenBAmount) + { + byte[] methodBuffer = new byte[25]; + + methodBuffer.WriteU8((byte)TokenSwapProgramInstructions.Values.DepositAllTokenTypes, 0); + methodBuffer.WriteU64(poolTokenAmount, 1); + methodBuffer.WriteU64(maxTokenAAmount, 9); + methodBuffer.WriteU64(maxTokenBAmount, 17); + + return methodBuffer; + } + + /// + /// Encode the transaction instruction data for the method. + /// + /// The amount of tokens in. + /// The maminx amount of tokens A. + /// The min amount of tokens B. + /// The byte array with the encoded data. + internal static byte[] EncodeWithdrawAllTokenTypesData(ulong poolTokenAmount, ulong minTokenAAmount, ulong minTokenBAmount) + { + byte[] methodBuffer = new byte[25]; + + methodBuffer.WriteU8((byte)TokenSwapProgramInstructions.Values.WithdrawAllTokenTypes, 0); + methodBuffer.WriteU64(poolTokenAmount, 1); + methodBuffer.WriteU64(minTokenAAmount, 9); + methodBuffer.WriteU64(minTokenBAmount, 17); + + return methodBuffer; + } + + /// + /// Encode the transaction instruction data for the method. + /// + /// The amount of tokens in. + /// The min amount of pool tokens out. + /// The byte array with the encoded data. + internal static byte[] EncodeDepositSingleTokenTypeExactAmountInData(ulong sourceTokenAmount, ulong minPoolTokenAmount) + { + byte[] methodBuffer = new byte[17]; + + methodBuffer.WriteU8((byte)TokenSwapProgramInstructions.Values.DepositSingleTokenTypeExactAmountIn, 0); + methodBuffer.WriteU64(sourceTokenAmount, 1); + methodBuffer.WriteU64(minPoolTokenAmount, 9); + + return methodBuffer; + } + + /// + /// Encode the transaction instruction data for the method. + /// + /// The amount of tokens out. + /// The max amount of pool tokens in. + /// The byte array with the encoded data. + internal static byte[] EncodeWithdrawSingleTokenTypeExactAmountOutData(ulong destTokenAmount, ulong maxPoolTokenAmount) + { + byte[] methodBuffer = new byte[17]; + + methodBuffer.WriteU8((byte)TokenSwapProgramInstructions.Values.WithdrawSingleTokenTypeExactAmountOut, 0); + methodBuffer.WriteU64(destTokenAmount, 1); + methodBuffer.WriteU64(maxPoolTokenAmount, 9); + + return methodBuffer; + } + + /// + /// Decodes the instruction instruction data for the method + /// + /// The decoded instruction to add data to. + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + internal static void DecodeInitializeData(DecodedInstruction decodedInstruction, ReadOnlySpan data, + IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Token Swap Account", keys[keyIndices[0]]); + decodedInstruction.Values.Add("Swap Authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("Token A Account", keys[keyIndices[2]]); + decodedInstruction.Values.Add("Token B Account", keys[keyIndices[3]]); + decodedInstruction.Values.Add("Pool Token Mint", keys[keyIndices[4]]); + decodedInstruction.Values.Add("Pool Token Fee Account", keys[keyIndices[5]]); + decodedInstruction.Values.Add("Pool Token Account", keys[keyIndices[6]]); + decodedInstruction.Values.Add("Token Program ID", keys[keyIndices[7]]); + + decodedInstruction.Values.Add("Nonce", data.GetU8(1)); + decodedInstruction.Values.Add("Trade Fee Numerator", data.GetU64(2)); + decodedInstruction.Values.Add("Trade Fee Denominator", data.GetU64(10)); + decodedInstruction.Values.Add("Owner Trade Fee Numerator", data.GetU64(18)); + decodedInstruction.Values.Add("Owner Trade Fee Denominator", data.GetU64(26)); + decodedInstruction.Values.Add("Owner Withraw Fee Numerator", data.GetU64(34)); + decodedInstruction.Values.Add("Owner Withraw Fee Denominator", data.GetU64(42)); + decodedInstruction.Values.Add("Host Fee Numerator", data.GetU64(50)); + decodedInstruction.Values.Add("Host Fee Denominator", data.GetU64(58)); + decodedInstruction.Values.Add("Curve Type", data.GetU64(59)); + //nothing to show for calculator unless hardcoding the switch stmt + } + + /// + /// Decodes the instruction instruction data for the method + /// + /// The decoded instruction to add data to. + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + internal static void DecodeSwapData(DecodedInstruction decodedInstruction, ReadOnlySpan data, + IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Token Swap Account", keys[keyIndices[0]]); + decodedInstruction.Values.Add("Swap Authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("User Transfer Authority", keys[keyIndices[2]]); + decodedInstruction.Values.Add("User Source Account", keys[keyIndices[3]]); + decodedInstruction.Values.Add("Token Base Into Account", keys[keyIndices[4]]); + decodedInstruction.Values.Add("Token Base From Account", keys[keyIndices[5]]); + decodedInstruction.Values.Add("User Destination Account", keys[keyIndices[6]]); + decodedInstruction.Values.Add("Pool Token Mint", keys[keyIndices[7]]); + decodedInstruction.Values.Add("Fee Account", keys[keyIndices[8]]); + decodedInstruction.Values.Add("Token Program ID", keys[keyIndices[9]]); + if (keyIndices.Length >= 11) + { + decodedInstruction.Values.Add("Host Fee Account", keys[keyIndices[10]]); + } + + decodedInstruction.Values.Add("Amount In", data.GetU64(1)); + decodedInstruction.Values.Add("Amount Out", data.GetU64(9)); + } + + /// + /// Decodes the instruction instruction data for the method + /// + /// The decoded instruction to add data to. + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + internal static void DecodeDepositAllTokenTypesData(DecodedInstruction decodedInstruction, ReadOnlySpan data, + IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Token Swap Account", keys[keyIndices[0]]); + decodedInstruction.Values.Add("Swap Authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("User Transfer Authority", keys[keyIndices[2]]); + decodedInstruction.Values.Add("User Token A Account", keys[keyIndices[3]]); + decodedInstruction.Values.Add("User Token B Account", keys[keyIndices[4]]); + decodedInstruction.Values.Add("Pool Token A Account", keys[keyIndices[5]]); + decodedInstruction.Values.Add("Pool Token B Account", keys[keyIndices[6]]); + decodedInstruction.Values.Add("Pool Token Mint", keys[keyIndices[7]]); + decodedInstruction.Values.Add("User Pool Token Account", keys[keyIndices[8]]); + decodedInstruction.Values.Add("Token Program ID", keys[keyIndices[9]]); + + decodedInstruction.Values.Add("Pool Tokens", data.GetU64(1)); + decodedInstruction.Values.Add("Max Token A", data.GetU64(9)); + decodedInstruction.Values.Add("Max Token B", data.GetU64(17)); + } + + /// + /// Decodes the instruction instruction data for the method + /// + /// The decoded instruction to add data to. + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + internal static void DecodeWithdrawAllTokenTypesData(DecodedInstruction decodedInstruction, ReadOnlySpan data, + IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Token Swap Account", keys[keyIndices[0]]); + decodedInstruction.Values.Add("Swap Authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("User Transfer Authority", keys[keyIndices[2]]); + decodedInstruction.Values.Add("Pool Token Account", keys[keyIndices[3]]); + decodedInstruction.Values.Add("User Pool Token Account", keys[keyIndices[4]]); + decodedInstruction.Values.Add("Pool Token A Account", keys[keyIndices[5]]); + decodedInstruction.Values.Add("Pool Token B Account", keys[keyIndices[6]]); + decodedInstruction.Values.Add("User Token A Account", keys[keyIndices[7]]); + decodedInstruction.Values.Add("User Token B Account", keys[keyIndices[8]]); + decodedInstruction.Values.Add("Fee Account", keys[keyIndices[9]]); + decodedInstruction.Values.Add("Token Program ID", keys[keyIndices[10]]); + + decodedInstruction.Values.Add("Pool Tokens", data.GetU64(1)); + decodedInstruction.Values.Add("Min Token A", data.GetU64(9)); + decodedInstruction.Values.Add("Min Token B", data.GetU64(17)); + } + + /// + /// Decodes the instruction instruction data for the method + /// + /// The decoded instruction to add data to. + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + internal static void DecodeDepositSingleTokenTypeExactAmountInData(DecodedInstruction decodedInstruction, ReadOnlySpan data, + IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Token Swap Account", keys[keyIndices[0]]); + decodedInstruction.Values.Add("Swap Authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("User Transfer Authority", keys[keyIndices[2]]); + decodedInstruction.Values.Add("User Source Token Account", keys[keyIndices[3]]); + decodedInstruction.Values.Add("Token A Swap Account", keys[keyIndices[4]]); + decodedInstruction.Values.Add("Token B Swap Account", keys[keyIndices[5]]); + decodedInstruction.Values.Add("Pool Mint Account", keys[keyIndices[6]]); + decodedInstruction.Values.Add("User Pool Token Account", keys[keyIndices[7]]); + decodedInstruction.Values.Add("Token Program ID", keys[keyIndices[8]]); + + decodedInstruction.Values.Add("Source Token Amount", data.GetU64(1)); + decodedInstruction.Values.Add("Min Pool Token Amount", data.GetU64(9)); + } + + /// + /// Decodes the instruction instruction data for the method + /// + /// The decoded instruction to add data to. + /// The instruction data to decode. + /// The account keys present in the transaction. + /// The indices of the account keys for the instruction as they appear in the transaction. + internal static void DecodeWithdrawSingleTokenTypeExactAmountOutData(DecodedInstruction decodedInstruction, ReadOnlySpan data, + IList keys, byte[] keyIndices) + { + decodedInstruction.Values.Add("Token Swap Account", keys[keyIndices[0]]); + decodedInstruction.Values.Add("Swap Authority", keys[keyIndices[1]]); + decodedInstruction.Values.Add("User Transfer Authority", keys[keyIndices[2]]); + decodedInstruction.Values.Add("Pool Mint Account", keys[keyIndices[3]]); + decodedInstruction.Values.Add("User Pool Token Account", keys[keyIndices[4]]); + decodedInstruction.Values.Add("Token A Swap Account", keys[keyIndices[5]]); + decodedInstruction.Values.Add("Token B Swap Account", keys[keyIndices[6]]); + decodedInstruction.Values.Add("User Token Account", keys[keyIndices[7]]); + decodedInstruction.Values.Add("Fee Account", keys[keyIndices[8]]); + decodedInstruction.Values.Add("Token Program ID", keys[keyIndices[9]]); + + decodedInstruction.Values.Add("Destination Token Amount", data.GetU64(1)); + decodedInstruction.Values.Add("Max Pool Token Amount", data.GetU64(9)); + } + } +} \ No newline at end of file diff --git a/src/Solnet.Programs/TokenSwap/TokenSwapProgramInstructions.cs b/src/Solnet.Programs/TokenSwap/TokenSwapProgramInstructions.cs new file mode 100644 index 00000000..65767732 --- /dev/null +++ b/src/Solnet.Programs/TokenSwap/TokenSwapProgramInstructions.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; + +namespace Solnet.Programs.TokenSwap +{ + /// + /// Represents the instruction types for the along with a friendly name so as not to use reflection. + /// + /// For more information see: + /// https://spl.solana.com/token-swap + /// https://docs.rs/spl-token-swap/2.1.0/spl_token_swap/ + /// + /// + internal static class TokenSwapProgramInstructions + { + /// + /// Represents the user-friendly names for the instruction types for the . + /// + internal static readonly Dictionary Names = new() + { + { Values.Initialize, "Initialize Swap" }, + { Values.Swap, "Swap" }, + { Values.DepositAllTokenTypes, "Deposit Both" }, + { Values.WithdrawAllTokenTypes, "Withdraw Both" }, + { Values.DepositSingleTokenTypeExactAmountIn, "Deposit Single" }, + { Values.WithdrawSingleTokenTypeExactAmountOut, "Withdraw Single" }, + }; + + /// + /// Represents the instruction types for the . + /// + internal enum Values : byte + { + /// + /// Initializes a new swap. + /// + Initialize = 0, + + /// + /// Swap the tokens in the pool. + /// + Swap = 1, + + /// + /// Deposit both types of tokens into the pool. + /// + DepositAllTokenTypes = 2, + + /// + /// Withdraw both types of tokens from the pool at the current ratio. + /// + WithdrawAllTokenTypes = 3, + + /// + /// Deposit one type of tokens into the pool. + /// + DepositSingleTokenTypeExactAmountIn = 4, + + /// + /// Withdraw one token type from the pool at the current ratio. + /// + WithdrawSingleTokenTypeExactAmountOut = 5, + + } + } +} \ No newline at end of file diff --git a/test/Solnet.Programs.Test/TokenSwapProgramTest.cs b/test/Solnet.Programs.Test/TokenSwapProgramTest.cs new file mode 100644 index 00000000..e59b313e --- /dev/null +++ b/test/Solnet.Programs.Test/TokenSwapProgramTest.cs @@ -0,0 +1,385 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Solnet.Programs.TokenSwap; +using Solnet.Programs.TokenSwap.Models; +using Solnet.Rpc.Models; +using Solnet.Wallet; +using System.Collections.Generic; +using System.Linq; + +namespace Solnet.Programs.Test +{ + [TestClass] + public class TokenSwapProgramTest + { + private const string MnemonicWords = + "route clerk disease box emerge airport loud waste attitude film army tray " + + "forward deal onion eight catalog surface unit card window walnut wealth medal"; + + private static readonly byte[] TokenSwapProgramIdBytes = + { + 6,165,58,174,54,191,72,111,181,217,56,38, + 78,230,69,215,75,96,22,224,244,122,235, + 179,236,22,67,139,247,191,251,225 + }; + + private static readonly byte[] ExpectedInitializeData = + { + 0,254,1,0,0,0,0,0,0,0,100,0,0,0,0,0,0,0,1,0,0,0,0,0, + 0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, + 0,1,0,0,0,0,0,0,0,232,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + }; + + private static readonly byte[] ExpectedSwapData = + { + 1,128,26,6,0,0,0,0,0,32,179,129,0,0,0,0,0 + }; + + private static readonly byte[] ExpectedDepositAllTokenTypesData = + { + 2,4,0,0,0,0,0,0,0,32,179,129,0,0,0,0,0,160,15,0,0,0,0,0,0 + }; + + private static readonly byte[] ExpectedWithdrawAllTokenTypesData = + { + 3,4,0,0,0,0,0,0,0,160,15,0,0,0,0,0,0,32,179,129,0,0,0,0,0 + }; + + private static readonly byte[] ExpectedDepositSingleTokenTypeExactAmountInData = + { + 4,160,15,0,0,0,0,0,0,4,0,0,0,0,0,0,0 + }; + private static readonly byte[] ExpectedWithdrawSingleTokenTypeExactAmountOutData = + { + 5,160,15,0,0,0,0,0,0,4,0,0,0,0,0,0,0 + }; + + private const string InitializeMessage = + "AgAHC1MuM7pUYPM9siiE2WjcHJ6uhumh/A9CE2nvOtqmyft3/FflD5yxhXv/GyRPQxWneSI1" + + "9VP2k43gUpVYG2jNHwarv91zFFZ0BDXh2dnixS0rka8rnVm8/lwluHEzfmVwaq9yV5EkRlspI5d" + + "TBei2pTw72+yOOEUXqFwgg1djn1hXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + + "AAAAAD0E4aXqh2tQhGa5IVDemLCaLk5I4fWxHtDzbxweno50QGqkt1zAcrZOVxGCNL6Xm7" + + "NI3/Bm+44+nxDHxEdV6rYjoSyYQV+btxvbXHxDsERTxTz2CLMUCdl3qxnNxEiIzEl6yl4BybR" + + "MuKQsQucwG8zcPF4h2aVMSq1AidCfnxnLgbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+" + + "/wCpBqU6rja/SG+12TgmTuZF10tgFuD0euuz7BZDi/e/++Hmyh7pP4homUV4nZbFzDiNooTfV0" + + "TICDNPFy0DXREIwgIEAgABNAAAAADAADAAAAAAAEQBAAAAAAAABqU6rja/SG+12TgmT" + + "uZF10tgFuD0euuz7BZDi/e/++EKCAEFBgcCCAMJYwD9GQAAAAAAAAAQJwAAAAAAAAUAA" + + "AAAAAAAECcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAABkAAAAAAAAAAAAA" + + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; + + private const string SwapMessage = + "AQAEC1MuM7pUYPM9siiE2WjcHJ6uhumh/A9CE2nvOtqmyft37QqyFDtQcH7hIYXKOEvkCQa+" + + "SmTK5A6OGMeeZooUoakBqpLdcwHK2TlcRgjS+l5uzSN/wZvuOPp8Qx8RHVeq2I6EsmEFfm7cb" + + "21x8Q7BEU8U89gizFAnZd6sZzcRIiMxwQavoWObAlxFe84OJSfFUsLJIhR4Q2+v+4N9Vt58Vla" + + "rv91zFFZ0BDXh2dnixS0rka8rnVm8/lwluHEzfmVwaiXrKXgHJtEy4pCxC5zAbzNw8XiHZpUxKr" + + "UCJ0J+fGcu/FflD5yxhXv/GyRPQxWneSI19VP2k43gUpVYG2jNHwb0E4aXqh2tQhGa5IVDemL" + + "CaLk5I4fWxHtDzbxweno50Qbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBqU6rja/SG" + + "+12TgmTuZF10tgFuD0euuz7BZDi/e/++H0144NBdw24rNWa3osyQqbSeyvVJGFXla9Rpj5nnnRRQ" + + "EKCgcIAAECAwQFBgkRAQDKmjsAAAAAIKEHAAAAAAA="; + + private const string DepositAllTokenTypesMessage = + "AQAEC1MuM7pUYPM9siiE2WjcHJ6uhumh/A9CE2nvOtqmyft37QqyFDtQcH7hIYXKOEvkCQa+S" + + "mTK5A6OGMeeZooUoanBBq+hY5sCXEV7zg4lJ8VSwskiFHhDb6/7g31W3nxWVgGqkt1zAcrZOV" + + "xGCNL6Xm7NI3/Bm+44+nxDHxEdV6rYjoSyYQV+btxvbXHxDsERTxTz2CLMUCdl3qxnNxEiIzGr" + + "v91zFFZ0BDXh2dnixS0rka8rnVm8/lwluHEzfmVwaq9yV5EkRlspI5dTBei2pTw72+yOOEUXqFwg" + + "g1djn1hX/FflD5yxhXv/GyRPQxWneSI19VP2k43gUpVYG2jNHwb0E4aXqh2tQhGa5IVDemLCaLk" + + "5I4fWxHtDzbxweno50Qbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBqU6rja/SG+12" + + "TgmTuZF10tgFuD0euuz7BZDi/e/++G/iXGArXvtQXqAznGhXSmATofHCuoBlpHxgPk4SfhBjwEKCg" + + "cIAAECAwQFBgkZAkBCDwAAAAAAAOh2SBcAAAAA6HZIFwAAAA=="; + + private const string WithdrawAllTokenTypesMessage = + "AQAEDFMuM7pUYPM9siiE2WjcHJ6uhumh/A9CE2nvOtqmyft3q7/dcxRWdAQ14dnZ4sUtK5GvK" + + "51ZvP5cJbhxM35lcGqvcleRJEZbKSOXUwXotqU8O9vsjjhFF6hcIINXY59YVwGqkt1zAcrZOVxGC" + + "NL6Xm7NI3/Bm+44+nxDHxEdV6rYjoSyYQV+btxvbXHxDsERTxTz2CLMUCdl3qxnNxEiIzHtCrI" + + "UO1BwfuEhhco4S+QJBr5KZMrkDo4Yx55mihShqcEGr6FjmwJcRXvODiUnxVLCySIUeENvr/uD" + + "fVbefFZWJespeAcm0TLikLELnMBvM3DxeIdmlTEqtQInQn58Zy78V+UPnLGFe/8bJE9DFad5Ij" + + "X1U/aTjeBSlVgbaM0fBvQThpeqHa1CEZrkhUN6YsJouTkjh9bEe0PNvHB6ejnRBt324ddloZPZy+" + + "FGzut5rBy0he1fWzeROoz1hX7/AKkGpTquNr9Ib7XZOCZO5kXXS2AW4PR667PsFkOL97/74RgZ" + + "qeIKqmWN9s3Opx7A0mQO3EPmMmA+8ndUoI0JQ3gfAQsLCAkAAQIDBAUGBwoZA0BCDwAA" + + "AAAA6AMAAAAAAADoAwAAAAAAAA=="; + + private const string DepositSingleTokenTypeExactAmountInMessage = + "AQAEClMuM7pUYPM9siiE2WjcHJ6uhumh/A9CE2nvOtqmyft37QqyFDtQcH7hIYXKOEvkCQa+" + + "SmTK5A6OGMeeZooUoakBqpLdcwHK2TlcRgjS+l5uzSN/wZvuOPp8Qx8RHVeq2I6EsmEFfm7c" + + "b21x8Q7BEU8U89gizFAnZd6sZzcRIiMxq7/dcxRWdAQ14dnZ4sUtK5GvK51ZvP5cJbhxM35lc" + + "GqvcleRJEZbKSOXUwXotqU8O9vsjjhFF6hcIINXY59YV/xX5Q+csYV7/xskT0MVp3kiNfVT9p" + + "ON4FKVWBtozR8G9BOGl6odrUIRmuSFQ3piwmi5OSOH1sR7Q828cHp6OdEG3fbh12Whk9nL4" + + "UbO63msHLSF7V9bN5E6jPWFfv8AqQalOq42v0hvtdk4Jk7mRddLYBbg9Hrrs+wWQ4v3v/vhzr" + + "NmDrCfcB0Cg6zcl3Vo7qSZvl3ypatPmPfURasFfUABCQkGBwABAgMEBQgRBADKmjsAAAAA6A" + + "MAAAAAAAA="; + + private const string WithdrawSingleTokenTypeExactAmountOutMessage = + "AQAEC1MuM7pUYPM9siiE2WjcHJ6uhumh/A9CE2nvOtqmyft3q7/dcxRWdAQ14dnZ4sUtK5Gv" + + "K51ZvP5cJbhxM35lcGqvcleRJEZbKSOXUwXotqU8O9vsjjhFF6hcIINXY59YVwGqkt1zAcrZOV" + + "xGCNL6Xm7NI3/Bm+44+nxDHxEdV6rYjoSyYQV+btxvbXHxDsERTxTz2CLMUCdl3qxnNxEiIz" + + "HtCrIUO1BwfuEhhco4S+QJBr5KZMrkDo4Yx55mihShqSXrKXgHJtEy4pCxC5zAbzNw8XiHZp" + + "UxKrUCJ0J+fGcu/FflD5yxhXv/GyRPQxWneSI19VP2k43gUpVYG2jNHwb0E4aXqh2tQhGa5I" + + "VDemLCaLk5I4fWxHtDzbxweno50Qbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBq" + + "U6rja/SG+12TgmTuZF10tgFuD0euuz7BZDi/e/++G6sYz49vuFr7rLN/dMfUEvpaHxP6DxaNZa" + + "SUp0zrIUswEKCgcIAAECAwQFBgkRBUBCDwAAAAAAoIYBAAAAAAA="; + + [TestMethod] + public void TestInitialize() + { + var wallet = new Wallet.Wallet(MnemonicWords); + + var tokenSwapAccount = wallet.GetAccount(1); + var tokenA = wallet.GetAccount(3); + var tokenB = wallet.GetAccount(4); + var poolMint = wallet.GetAccount(5); + var poolFee = wallet.GetAccount(6); + var poolToken = wallet.GetAccount(7); + + var txInstruction = new TokenSwapProgram(tokenSwapAccount).Initialize( + tokenA.PublicKey, + tokenB.PublicKey, + poolMint.PublicKey, + poolFee.PublicKey, + poolToken.PublicKey, + new Fees() + { + TradeFeeNumerator = 1, + TradeFeeDenominator = 100, + OwnerWithrawFeeNumerator = 0, + OwnerWithrawFeeDenomerator = 1, + OwnerTradeFeeNumerator = 1, + OwnerTradeFeeDenomerator = 100, + HostFeeNumerator = 1, + HostFeeDenomerator = 1000, + }, + SwapCurve.ConstantProduct + ); + + Assert.AreEqual(8, txInstruction.Keys.Count); + CollectionAssert.AreEqual(TokenSwapProgramIdBytes, txInstruction.ProgramId); + CollectionAssert.AreEqual(ExpectedInitializeData, txInstruction.Data); + } + + [TestMethod] + public void TestSwap() + { + var wallet = new Wallet.Wallet(MnemonicWords); + + var tokenSwapAccount = wallet.GetAccount(1); + var userXfer = wallet.GetAccount(3); + var source = wallet.GetAccount(4); + var into = wallet.GetAccount(5); + var from = wallet.GetAccount(6); + var destination = wallet.GetAccount(7); + var poolTokenMint = wallet.GetAccount(7); + var fee = wallet.GetAccount(7); + var hostFee = wallet.GetAccount(7); + + var txInstruction = new TokenSwapProgram(tokenSwapAccount).Swap( + userXfer.PublicKey, + source.PublicKey, + into.PublicKey, + from.PublicKey, + destination.PublicKey, + poolTokenMint.PublicKey, + fee.PublicKey, + hostFee.PublicKey, + 400_000, + 8_500_000 + ); + + Assert.AreEqual(11, txInstruction.Keys.Count); + CollectionAssert.AreEqual(TokenSwapProgramIdBytes, txInstruction.ProgramId); + CollectionAssert.AreEqual(ExpectedSwapData, txInstruction.Data); + } + + [TestMethod] + public void TestDepositAllTokenTypes() + { + var wallet = new Wallet.Wallet(MnemonicWords); + + var tokenSwapAccount = wallet.GetAccount(1); + var userXfer = wallet.GetAccount(3); + var authA = wallet.GetAccount(4); + var authB = wallet.GetAccount(5); + var baseA = wallet.GetAccount(6); + var baseB = wallet.GetAccount(7); + var poolTokenMint = wallet.GetAccount(7); + var poolAccount = wallet.GetAccount(7); + + var txInstruction = new TokenSwapProgram(tokenSwapAccount).DepositAllTokenTypes( + userXfer.PublicKey, + authA.PublicKey, + authB.PublicKey, + baseA.PublicKey, + baseB.PublicKey, + poolTokenMint.PublicKey, + poolAccount.PublicKey, + 4, + 8_500_000, + 4_000 + ); + + Assert.AreEqual(10, txInstruction.Keys.Count); + CollectionAssert.AreEqual(TokenSwapProgramIdBytes, txInstruction.ProgramId); + CollectionAssert.AreEqual(ExpectedDepositAllTokenTypesData, txInstruction.Data); + } + + [TestMethod] + public void TestWithdrawAllTokenTypes() + { + var wallet = new Wallet.Wallet(MnemonicWords); + + var tokenSwapAccount = wallet.GetAccount(1); + var userXfer = wallet.GetAccount(3); + var poolTokenMint = wallet.GetAccount(4); + var sourcePoolAccount = wallet.GetAccount(4); + var tokenAFrom = wallet.GetAccount(5); + var tokenBFrom = wallet.GetAccount(6); + var tokenATo = wallet.GetAccount(7); + var tokenBTo = wallet.GetAccount(7); + var feeAccount = wallet.GetAccount(7); + + var txInstruction = new TokenSwapProgram(tokenSwapAccount).WithdrawAllTokenTypes( + userXfer.PublicKey, + poolTokenMint.PublicKey, + sourcePoolAccount.PublicKey, + tokenAFrom.PublicKey, + tokenBFrom.PublicKey, + tokenATo.PublicKey, + tokenBTo.PublicKey, + feeAccount.PublicKey, + 4, + 4_000, + 8_500_000 + ); + + Assert.AreEqual(11, txInstruction.Keys.Count); + CollectionAssert.AreEqual(TokenSwapProgramIdBytes, txInstruction.ProgramId); + CollectionAssert.AreEqual(ExpectedWithdrawAllTokenTypesData, txInstruction.Data); + } + + [TestMethod] + public void TestDepositSingleTokenTypeExactAmountInTypes() + { + var wallet = new Wallet.Wallet(MnemonicWords); + + var tokenSwapAccount = wallet.GetAccount(1); + var userXfer = wallet.GetAccount(3); + var tokenSource = wallet.GetAccount(4); + var tokenA = wallet.GetAccount(5); + var tokenB = wallet.GetAccount(6); + var poolMint = wallet.GetAccount(7); + var pool = wallet.GetAccount(7); + + var txInstruction = new TokenSwapProgram(tokenSwapAccount).DepositSingleTokenTypeExactAmountIn( + userXfer.PublicKey, + tokenSource.PublicKey, + tokenA.PublicKey, + tokenB.PublicKey, + poolMint.PublicKey, + pool.PublicKey, + 4_000, + 4 + ); + + Assert.AreEqual(9, txInstruction.Keys.Count); + CollectionAssert.AreEqual(TokenSwapProgramIdBytes, txInstruction.ProgramId); + CollectionAssert.AreEqual(ExpectedDepositSingleTokenTypeExactAmountInData, txInstruction.Data); + } + + [TestMethod] + public void TestWithdrawSingleTokenTypeExactAmountOutTypes() + { + var wallet = new Wallet.Wallet(MnemonicWords); + + var tokenSwapAccount = wallet.GetAccount(1); + var userXfer = wallet.GetAccount(3); + var poolMint = wallet.GetAccount(4); + var sourcePool = wallet.GetAccount(5); + var tokenASource = wallet.GetAccount(6); + var tokenBSource = wallet.GetAccount(6); + var userToken = wallet.GetAccount(7); + var feeAccount = wallet.GetAccount(7); + + var txInstruction = new TokenSwapProgram(tokenSwapAccount).WithdrawSingleTokenTypeExactAmountOut( + userXfer.PublicKey, + poolMint.PublicKey, + sourcePool.PublicKey, + tokenASource.PublicKey, + tokenBSource.PublicKey, + userToken.PublicKey, + feeAccount.PublicKey, + 4_000, + 4 + ); + + Assert.AreEqual(10, txInstruction.Keys.Count); + CollectionAssert.AreEqual(TokenSwapProgramIdBytes, txInstruction.ProgramId); + CollectionAssert.AreEqual(ExpectedWithdrawSingleTokenTypeExactAmountOutData, txInstruction.Data); + } + + [TestMethod] + public void InitializeDecodeTest() + { + Message msg = Message.Deserialize(InitializeMessage); + List decodedInstructions = InstructionDecoder.DecodeInstructions(msg); + + Assert.AreEqual(2, decodedInstructions.Count); + Assert.AreEqual("[0] 11111111111111111111111111111111:System Program:Create Account\n[0] [[Owner Account, 6bhhceZToGG9RsTe1nfNFXEMjavhj6CV55EsvearAt2z],[New Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Amount, 3145920],[Space, 324]]\n[0] InnerInstructions (0)\n", + decodedInstructions[0].ToString()); + Assert.AreEqual("[0] SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8:Token Swap Program:Initialize Swap\n[0] [[Token Swap Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Swap Authority, HRmkKfXbHcvNhWHw47zqoexKiLHmowR8o7hdwwWdaHoW],[Token A Account, 7WGJswQpwuNePUiEFBqCMKnGcpkNoX7fFeAdM16o1wV],[Token B Account, AbLFYgniLdGWikGJX3dT4iTWoX1FbFBwu2sjGDQN7nfa],[Pool Token Mint, CZSQMnD4jTvRfEuApDAmjWvz1AWpFpXqoePPXwZpmk1F],[Pool Token Fee Account, 3Z24fqykBPn1wNSXGz7SA5MXqGGk3DPSDpmxQoERMHrM],[Pool Token Account, CosUN9gxk8M6gdSDHYvaKKKCbX2VL73z1mJ66tYFsnSA],[Token Program ID, TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA],[Nonce, 253],[Trade Fee Numerator, 25],[Trade Fee Denominator, 10000],[Owner Trade Fee Numerator, 5],[Owner Trade Fee Denominator, 10000],[Owner Withraw Fee Numerator, 0],[Owner Withraw Fee Denominator, 0],[Host Fee Numerator, 20],[Host Fee Denominator, 100],[Curve Type, 0]]\n[0] InnerInstructions (0)\n", + decodedInstructions[1].ToString()); + } + + [TestMethod] + public void SwapDecodeTest() + { + Message msg = Message.Deserialize(SwapMessage); + List decodedInstructions = InstructionDecoder.DecodeInstructions(msg); + + Assert.AreEqual(1, decodedInstructions.Count); + Assert.AreEqual("[0] SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8:Token Swap Program:Swap\n[0] [[Token Swap Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Swap Authority, HRmkKfXbHcvNhWHw47zqoexKiLHmowR8o7hdwwWdaHoW],[User Transfer Authority, 6bhhceZToGG9RsTe1nfNFXEMjavhj6CV55EsvearAt2z],[User Source Account, GxK5rLRGx1AnE9BZzQBP6SVenavuZqRUXbE6QTzL3jjW],[Token Base Into Account, 7WGJswQpwuNePUiEFBqCMKnGcpkNoX7fFeAdM16o1wV],[Token Base From Account, AbLFYgniLdGWikGJX3dT4iTWoX1FbFBwu2sjGDQN7nfa],[User Destination Account, DzVbjXqE9oFMJ4dWa9PqCA2bmiARtSURpmijux3PkC45],[Pool Token Mint, CZSQMnD4jTvRfEuApDAmjWvz1AWpFpXqoePPXwZpmk1F],[Fee Account, 3Z24fqykBPn1wNSXGz7SA5MXqGGk3DPSDpmxQoERMHrM],[Token Program ID, TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA],[Amount In, 1000000000],[Amount Out, 500000]]\n[0] InnerInstructions (0)\n", + decodedInstructions[0].ToString()); + } + + [TestMethod] + public void DepositAllTokenTypesDecodeTest() + { + Message msg = Message.Deserialize(DepositAllTokenTypesMessage); + List decodedInstructions = InstructionDecoder.DecodeInstructions(msg); + + Assert.AreEqual(1, decodedInstructions.Count); + Assert.AreEqual("[0] SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8:Token Swap Program:Deposit Both\n[0] [[Token Swap Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Swap Authority, HRmkKfXbHcvNhWHw47zqoexKiLHmowR8o7hdwwWdaHoW],[User Transfer Authority, 6bhhceZToGG9RsTe1nfNFXEMjavhj6CV55EsvearAt2z],[User Token A Account, GxK5rLRGx1AnE9BZzQBP6SVenavuZqRUXbE6QTzL3jjW],[User Token B Account, DzVbjXqE9oFMJ4dWa9PqCA2bmiARtSURpmijux3PkC45],[Pool Token A Account, 7WGJswQpwuNePUiEFBqCMKnGcpkNoX7fFeAdM16o1wV],[Pool Token B Account, AbLFYgniLdGWikGJX3dT4iTWoX1FbFBwu2sjGDQN7nfa],[Pool Token Mint, CZSQMnD4jTvRfEuApDAmjWvz1AWpFpXqoePPXwZpmk1F],[User Pool Token Account, CosUN9gxk8M6gdSDHYvaKKKCbX2VL73z1mJ66tYFsnSA],[Token Program ID, TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA],[Pool Tokens, 1000000],[Max Token A, 100000000000],[Max Token B, 100000000000]]\n[0] InnerInstructions (0)\n", + decodedInstructions[0].ToString()); + } + + [TestMethod] + public void WithdrawAllTokenTypesDecodeTest() + { + Message msg = Message.Deserialize(WithdrawAllTokenTypesMessage); + List decodedInstructions = InstructionDecoder.DecodeInstructions(msg); + + Assert.AreEqual(1, decodedInstructions.Count); + Assert.AreEqual("[0] SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8:Token Swap Program:Withdraw Both\n[0] [[Token Swap Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Swap Authority, HRmkKfXbHcvNhWHw47zqoexKiLHmowR8o7hdwwWdaHoW],[User Transfer Authority, 6bhhceZToGG9RsTe1nfNFXEMjavhj6CV55EsvearAt2z],[Pool Token Account, CZSQMnD4jTvRfEuApDAmjWvz1AWpFpXqoePPXwZpmk1F],[User Pool Token Account, CosUN9gxk8M6gdSDHYvaKKKCbX2VL73z1mJ66tYFsnSA],[Pool Token A Account, 7WGJswQpwuNePUiEFBqCMKnGcpkNoX7fFeAdM16o1wV],[Pool Token B Account, AbLFYgniLdGWikGJX3dT4iTWoX1FbFBwu2sjGDQN7nfa],[User Token A Account, GxK5rLRGx1AnE9BZzQBP6SVenavuZqRUXbE6QTzL3jjW],[User Token B Account, DzVbjXqE9oFMJ4dWa9PqCA2bmiARtSURpmijux3PkC45],[Fee Account, 3Z24fqykBPn1wNSXGz7SA5MXqGGk3DPSDpmxQoERMHrM],[Token Program ID, TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA],[Pool Tokens, 1000000],[Min Token A, 1000],[Min Token B, 1000]]\n[0] InnerInstructions (0)\n", + decodedInstructions[0].ToString()); + } + + [TestMethod] + public void DepositSingleTokenTypeExactAmountInDecodeTest() + { + Message msg = Message.Deserialize(DepositSingleTokenTypeExactAmountInMessage); + List decodedInstructions = InstructionDecoder.DecodeInstructions(msg); + + Assert.AreEqual(1, decodedInstructions.Count); + Assert.AreEqual("[0] SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8:Token Swap Program:Deposit Single\n[0] [[Token Swap Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Swap Authority, HRmkKfXbHcvNhWHw47zqoexKiLHmowR8o7hdwwWdaHoW],[User Transfer Authority, 6bhhceZToGG9RsTe1nfNFXEMjavhj6CV55EsvearAt2z],[User Source Token Account, GxK5rLRGx1AnE9BZzQBP6SVenavuZqRUXbE6QTzL3jjW],[Token A Swap Account, 7WGJswQpwuNePUiEFBqCMKnGcpkNoX7fFeAdM16o1wV],[Token B Swap Account, AbLFYgniLdGWikGJX3dT4iTWoX1FbFBwu2sjGDQN7nfa],[Pool Mint Account, CZSQMnD4jTvRfEuApDAmjWvz1AWpFpXqoePPXwZpmk1F],[User Pool Token Account, CosUN9gxk8M6gdSDHYvaKKKCbX2VL73z1mJ66tYFsnSA],[Token Program ID, TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA],[Source Token Amount, 1000000000],[Min Pool Token Amount, 1000]]\n[0] InnerInstructions (0)\n", + decodedInstructions[0].ToString()); + } + + [TestMethod] + public void WithdrawSingleTokenTypeExactAmountOutDecodeTest() + { + Message msg = Message.Deserialize(WithdrawSingleTokenTypeExactAmountOutMessage); + List decodedInstructions = InstructionDecoder.DecodeInstructions(msg); + + Assert.AreEqual(1, decodedInstructions.Count); + Assert.AreEqual("[0] SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8:Token Swap Program:Withdraw Single\n[0] [[Token Swap Account, Hz3UWwAR4z7TZmzMW2TFjjzDtxEveiZZbJ4sg1LEuvKo],[Swap Authority, HRmkKfXbHcvNhWHw47zqoexKiLHmowR8o7hdwwWdaHoW],[User Transfer Authority, 6bhhceZToGG9RsTe1nfNFXEMjavhj6CV55EsvearAt2z],[Pool Mint Account, CZSQMnD4jTvRfEuApDAmjWvz1AWpFpXqoePPXwZpmk1F],[User Pool Token Account, CosUN9gxk8M6gdSDHYvaKKKCbX2VL73z1mJ66tYFsnSA],[Token A Swap Account, 7WGJswQpwuNePUiEFBqCMKnGcpkNoX7fFeAdM16o1wV],[Token B Swap Account, AbLFYgniLdGWikGJX3dT4iTWoX1FbFBwu2sjGDQN7nfa],[User Token Account, GxK5rLRGx1AnE9BZzQBP6SVenavuZqRUXbE6QTzL3jjW],[Fee Account, 3Z24fqykBPn1wNSXGz7SA5MXqGGk3DPSDpmxQoERMHrM],[Token Program ID, TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA],[Destination Token Amount, 1000000],[Max Pool Token Amount, 100000]]\n[0] InnerInstructions (0)\n", + decodedInstructions[0].ToString()); + } + } +} \ No newline at end of file