defmodule Solana.SPL.TokenSwap do
@moduledoc """
Functions for interacting with Solana's [Token Swap
Program](https://spl.solana.com/token-swap).
"""
alias Solana.{Instruction, Account, SystemProgram}
import Solana.Helpers
@curves [:product, :price, :stable, :offset]
@doc """
The Token Swap Program's ID.
"""
@spec id() :: binary
def id(), do: Solana.pubkey!("SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8")
@doc """
The size of a serialized token swap account.
"""
@spec byte_size() :: pos_integer
def byte_size(), do: 324
@doc """
Translates the result of a `Solana.RPC.Request.get_account_info/2` into
token swap account information.
"""
@spec from_account_info(info :: map) :: map | :error
def from_account_info(info)
def from_account_info(%{"data" => [data, "base64"]}) do
case Base.decode64(data) do
{:ok, decoded} when byte_size(decoded) == 324 ->
[<<vsn>>, <<init>>, <<seed>>, keys, fees, <<type>>, <<params::integer-size(256)-little>>] =
chunk(decoded, [1, 1, 1, 7 * 32, 8 * 8, 1, 32])
[_, token_a, token_b, pool_mint, mint_a, mint_b, fee_account] = chunk(keys, 32)
[trade_fee, owner_trade_fee, owner_withdraw_fee, host_fee] =
fees
|> chunk(8)
|> Enum.map(fn <<n::integer-size(64)-little>> -> n end)
|> Enum.chunk_every(2)
|> Enum.map(&List.to_tuple/1)
%{
token_a: token_a,
token_b: token_b,
trade_fee: trade_fee,
owner_trade_fee: owner_trade_fee,
owner_withdraw_fee: owner_withdraw_fee,
host_fee: host_fee,
pool_mint: pool_mint,
mint_a: mint_a,
mint_b: mint_b,
fee_account: fee_account,
version: vsn,
initialized?: init == 1,
bump_seed: seed,
curve: {Enum.at(@curves, type), params}
}
_other ->
:error
end
end
def from_account_info(_), do: :error
@doc false
def validate_fee(f = {n, d})
when is_integer(n) and n > 0 and is_integer(d) and d > 0 do
{:ok, f}
end
def validate_fee({_n, 0}), do: {:error, "fee denominator cannot be 0"}
def validate_fee(f), do: {:error, "expected a fee, got: #{inspect(f)}"}
@doc false
def validate_curve({type, params}) when type in @curves do
{:ok, {type, params}}
end
def validate_curve(type) when type in @curves, do: {:ok, {type, 0}}
def validate_curve(c) do
{:error, "expected a curve in #{inspect(@curves)}, got: #{inspect(c)}"}
end
@init_schema [
payer: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The account that will pay for the token swap account creation."
],
balance: [
type: :non_neg_integer,
required: true,
doc: "The lamport balance the token swap account should have."
],
authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token swap account's swap authority"
],
new: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The public key of the newly-created token swap account."
],
token_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `A` token account in token swaps. Must be owned by `authority`."
],
token_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `B` token account in token swaps. Must be owned by `authority`."
],
pool: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token account which holds outside liquidity and enables A/B trades."
],
pool_mint: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The mint of the `pool`."
],
fee_account: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token account which receives all trading and withdrawal fees."
],
trade_fee: [
type: {:custom, __MODULE__, :validate_fee, []},
default: {0, 1},
doc: """
The `new` swap account's trading fee. Trade fees are extra token amounts
that are held inside the token accounts during a trade, making the value
of liquidity tokens rise.
"""
],
owner_trade_fee: [
type: {:custom, __MODULE__, :validate_fee, []},
default: {0, 1},
doc: """
The `new` swap account's owner trading fee. Owner trading fees are extra
token amounts that are held inside the token accounts during a trade, with
the equivalent in pool tokens minted to the owner of the program.
"""
],
owner_withdraw_fee: [
type: {:custom, __MODULE__, :validate_fee, []},
default: {0, 1},
doc: """
The `new` swap account's owner withdraw fee. Owner withdraw fees are extra
liquidity pool token amounts that are sent to the owner on every
withdrawal.
"""
],
host_fee: [
type: {:custom, __MODULE__, :validate_fee, []},
default: {0, 1},
doc: """
The `new` swap account's host fee. Host fees are a proportion of the
owner trading fees, sent to an extra account provided during the trade.
"""
],
curve: [
type: {:custom, __MODULE__, :validate_curve, []},
required: true,
doc: """
The automated market maker (AMM) curve to use for the `new` token swap account.
Should take the form `{type, params}`. See [the
docs](https://spl.solana.com/token-swap#curves) on which curves are available.
"""
]
]
@doc """
Creates the instructions to initialize a new token swap account.
## Options
#{NimbleOptions.docs(@init_schema)}
"""
def init(opts) do
case validate(opts, @init_schema) do
{:ok, params} ->
[
SystemProgram.create_account(
lamports: params.balance,
space: byte_size(),
from: params.payer,
new: params.new,
program_id: id()
),
initialize_ix(params)
]
error ->
error
end
end
defp initialize_ix(params) do
%Instruction{
program: id(),
accounts: [
%Account{key: params.new, writable?: true},
%Account{key: params.authority},
%Account{key: params.token_a},
%Account{key: params.token_b},
%Account{key: params.pool_mint, writable?: true},
%Account{key: params.fee_account},
%Account{key: params.pool, writable?: true},
%Account{key: Solana.SPL.Token.id()}
],
data: Instruction.encode_data(initialize_data(params))
}
end
defp initialize_data(params = %{curve: {type, parameters}}) do
[
0,
encode_fee(params.trade_fee),
encode_fee(params.owner_trade_fee),
encode_fee(params.owner_withdraw_fee),
encode_fee(params.host_fee),
Enum.find_index(@curves, &(&1 == type)),
{parameters, 32 * 8}
]
|> List.flatten()
end
defp encode_fee({n, d}), do: [{n, 64}, {d, 64}]
@swap_schema [
swap: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token swap to use."
],
authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "the `swap` account's swap authority."
],
user_source: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "User's source token account. Must have the same mint as `swap_source`."
],
swap_source: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "`swap` source token account. Must have the same mint as `user_source`."
],
user_destination: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "User's destination token account. Must have the same mint as `swap_destination`."
],
swap_destination: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "`swap` destination token account. Must have the same mint as `user_destination`."
],
pool_mint: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` pool token's mint."
],
fee_account: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token account which receives all trading and withdrawal fees."
],
host_fee_account: [
type: {:custom, Solana.Key, :check, []},
doc: "Host account to gather fees."
],
user_authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "Account delegated to transfer the user's tokens."
],
amount: [
type: :pos_integer,
required: true,
doc: "Amount to transfer from the source account."
],
minimum_return: [
type: :pos_integer,
required: true,
doc: "Minimum number of tokens the user will receive."
]
]
@doc """
Creates the instructions to swap token `A` for token `B` or vice versa.
## Options
#{NimbleOptions.docs(@swap_schema)}
"""
def swap(opts) do
case validate(opts, @swap_schema) do
{:ok, params} ->
%Instruction{
program: id(),
accounts:
List.flatten([
%Account{key: params.swap},
%Account{key: params.authority},
%Account{key: params.user_authority, signer?: true},
%Account{key: params.user_source, writable?: true},
%Account{key: params.swap_source, writable?: true},
%Account{key: params.swap_destination, writable?: true},
%Account{key: params.user_destination, writable?: true},
%Account{key: params.pool_mint, writable?: true},
%Account{key: params.fee_account, writable?: true},
%Account{key: Solana.SPL.Token.id()},
host_fee_account(params)
]),
data: Instruction.encode_data([1, {params.amount, 64}, {params.minimum_return, 64}])
}
error ->
error
end
end
defp host_fee_account(%{host_fee_account: key}) do
[%Account{key: key, writable?: true}]
end
defp host_fee_account(_), do: []
@deposit_all_schema [
swap: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token swap to use."
],
authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "the `swap` account's swap authority."
],
user_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for token `A`."
],
user_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for token `B`."
],
swap_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `A`."
],
swap_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `B`."
],
user_pool: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for the pool token. Pool tokens will be deposited here."
],
pool_mint: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` pool token's mint."
],
user_authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "Account delegated to transfer the user's tokens."
],
amount_a: [
type: :pos_integer,
required: true,
doc: "Maximum amount of token `A` to deposit."
],
amount_b: [
type: :pos_integer,
required: true,
doc: "Maximum amount of token `B` to deposit."
],
amount_pool: [
type: :pos_integer,
required: true,
doc: "Amount of pool tokens to mint."
]
]
@doc """
Creates the instructions to deposit both `A` and `B` tokens into the pool.
## Options
#{NimbleOptions.docs(@deposit_all_schema)}
"""
def deposit_all(opts) do
case validate(opts, @deposit_all_schema) do
{:ok, params} ->
%Instruction{
program: id(),
accounts:
List.flatten([
%Account{key: params.swap},
%Account{key: params.authority},
%Account{key: params.user_authority, signer?: true},
%Account{key: params.user_a, writable?: true},
%Account{key: params.user_b, writable?: true},
%Account{key: params.swap_a, writable?: true},
%Account{key: params.swap_b, writable?: true},
%Account{key: params.pool_mint, writable?: true},
%Account{key: params.user_pool, writable?: true},
%Account{key: Solana.SPL.Token.id()}
]),
data:
Instruction.encode_data([
2,
{params.amount_pool, 64},
{params.amount_a, 64},
{params.amount_b, 64}
])
}
error ->
error
end
end
@withdraw_all_schema [
swap: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token swap to use."
],
authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "the `swap` account's swap authority."
],
user_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for token `A`."
],
user_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for token `B`."
],
swap_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `A`."
],
swap_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `B`."
],
user_pool: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for the pool token. Pool tokens with be withdrawn from here."
],
pool_mint: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` pool token's mint."
],
user_authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "Account delegated to transfer the user's tokens."
],
fee_account: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token account which receives all trading and withdrawal fees."
],
amount_a: [
type: :pos_integer,
required: true,
doc: "Minimum amount of token `A` to withdraw."
],
amount_b: [
type: :pos_integer,
required: true,
doc: "Minimum amount of token `B` to withdraw."
],
amount_pool: [
type: :pos_integer,
required: true,
doc: "Amount of pool tokens to burn."
]
]
@doc """
Creates the instructions to withdraw both `A` and `B` tokens from the pool.
## Options
#{NimbleOptions.docs(@withdraw_all_schema)}
"""
def withdraw_all(opts) do
case validate(opts, @withdraw_all_schema) do
{:ok, params} ->
%Instruction{
program: id(),
accounts:
List.flatten([
%Account{key: params.swap},
%Account{key: params.authority},
%Account{key: params.user_authority, signer?: true},
%Account{key: params.pool_mint, writable?: true},
%Account{key: params.user_pool, writable?: true},
%Account{key: params.swap_a, writable?: true},
%Account{key: params.swap_b, writable?: true},
%Account{key: params.user_a, writable?: true},
%Account{key: params.user_b, writable?: true},
%Account{key: params.fee_account, writable?: true},
%Account{key: Solana.SPL.Token.id()}
]),
data:
Instruction.encode_data([
3,
{params.amount_pool, 64},
{params.amount_a, 64},
{params.amount_b, 64}
])
}
error ->
error
end
end
@deposit_schema [
swap: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token swap to use."
],
authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "the `swap` account's swap authority."
],
user_token: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for token `A` or `B`."
],
swap_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `A`."
],
swap_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `B`."
],
user_pool: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for the pool token. Pool tokens will be deposited here."
],
pool_mint: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` pool token's mint."
],
user_authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "Account delegated to transfer the user's tokens."
],
amount: [
type: :pos_integer,
required: true,
doc: "Amount of token `A` or `B` to deposit."
],
amount_pool: [
type: :pos_integer,
required: true,
doc: "Minimum amount of pool tokens to mint."
]
]
@doc """
Creates the instructions to deposit `A` or `B` tokens into the pool.
## Options
#{NimbleOptions.docs(@deposit_schema)}
"""
def deposit(opts) do
case validate(opts, @deposit_schema) do
{:ok, params} ->
%Instruction{
program: id(),
accounts:
List.flatten([
%Account{key: params.swap},
%Account{key: params.authority},
%Account{key: params.user_authority, signer?: true},
%Account{key: params.user_token, writable?: true},
%Account{key: params.swap_a, writable?: true},
%Account{key: params.swap_b, writable?: true},
%Account{key: params.pool_mint, writable?: true},
%Account{key: params.user_pool, writable?: true},
%Account{key: Solana.SPL.Token.id()}
]),
data: Instruction.encode_data([4, {params.amount, 64}, {params.amount_pool, 64}])
}
error ->
error
end
end
@withdraw_schema [
swap: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token swap to use."
],
authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "the `swap` account's swap authority."
],
user_token: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for token `A` or `B`."
],
swap_a: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `A`."
],
swap_b: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` account for token `B`."
],
user_pool: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The user's account for the pool token. Pool tokens with be withdrawn from here."
],
pool_mint: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The `swap` pool token's mint."
],
user_authority: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "Account delegated to transfer the user's tokens."
],
fee_account: [
type: {:custom, Solana.Key, :check, []},
required: true,
doc: "The token account which receives all trading and withdrawal fees."
],
amount: [
type: :pos_integer,
required: true,
doc: "Amount of token `A` or `B` to withdraw."
],
amount_pool: [
type: :pos_integer,
required: true,
doc: "Maximum amount of pool tokens to burn."
]
]
@doc """
Creates the instructions to withdraw `A` or `B` tokens from the pool.
## Options
#{NimbleOptions.docs(@withdraw_schema)}
"""
def withdraw(opts) do
case validate(opts, @withdraw_schema) do
{:ok, params} ->
%Instruction{
program: id(),
accounts:
List.flatten([
%Account{key: params.swap},
%Account{key: params.authority},
%Account{key: params.user_authority, signer?: true},
%Account{key: params.pool_mint, writable?: true},
%Account{key: params.user_pool, writable?: true},
%Account{key: params.swap_a, writable?: true},
%Account{key: params.swap_b, writable?: true},
%Account{key: params.user_token, writable?: true},
%Account{key: params.fee_account, writable?: true},
%Account{key: Solana.SPL.Token.id()}
]),
data: Instruction.encode_data([5, {params.amount, 64}, {params.amount_pool, 64}])
}
error ->
error
end
end
end