Skip to content

Commit

Permalink
Merge pull request #98 from kommitters/v0.3
Browse files Browse the repository at this point in the history
v0.3.0 release
  • Loading branch information
Juan Hurtado authored Feb 15, 2022
2 parents 8c7fa99 + 9b1daaa commit 6d0afe3
Show file tree
Hide file tree
Showing 26 changed files with 504 additions and 77 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.3.0 (11.02.2022)
* CAP-0027 Add suport for multiplexed accounts.

## 0.2.0 (07.02.2022)
* Add base documentation and initial examples.
* Dynamically update the transaction's BaseFee.
Expand Down
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This library is aimed at developers building Elixir applications that interact w
```elixir
def deps do
[
{:stellar_sdk, "~> 0.2.0"}
{:stellar_sdk, "~> 0.3.0"}
]
end
```
Expand All @@ -44,6 +44,40 @@ config :stellar_sdk, :http_client_impl, YourApp.CustomClientImpl

## Usage

### Accounts
Accounts are the central data structure in Stellar. They hold balances, sign transactions, and issue assets. All entries that persist on the ledger are owned by a particular account.

```elixir
# initialize an account
account = Stellar.TxBuild.Account.new("GDC3W2X5KUTZRTQIKXM5D2I5WG5JYSEJQWEELVPQ5YMWZR6CA2JJ35RW")

```

### Muxed accounts
A **muxed** (or **multiplexed**) account is an account that exists “virtually” under a traditional Stellar account address. It combines the familiar `GABC...` address with a `64-bit` integer `ID` and can be used to distinguish multiple “virtual” accounts that share an underlying “real” account. More details in [**CAP-27**][stellar-cap-27].

```elixir
# initialize an account with a muxed address
account = Stellar.TxBuild.Account.new("MBXV5U2D67J7HUW42JKBGD4WNZON4SOPXXDFTYQ7BCOG5VCARGCRMAAAAAAAAAAAARKPQ")

account.account_id
# GBXV5U2D67J7HUW42JKBGD4WNZON4SOPXXDFTYQ7BCOG5VCARGCRMQQH

account.muxed_id
# 4

```
#### Create a muxed account

```elixir
# create a muxed account
account = Stellar.TxBuild.Account.create_muxed("GBXV5U2D67J7HUW42JKBGD4WNZON4SOPXXDFTYQ7BCOG5VCARGCRMQQH", 4)

account.address
# MBXV5U2D67J7HUW42JKBGD4WNZON4SOPXXDFTYQ7BCOG5VCARGCRMAAAAAAAAAAAARKPQ

```

### KeyPairs
Stellar relies on public key cryptography to ensure that transactions are secure: every account requires a valid keypair consisting of a public key and a private key.

Expand Down Expand Up @@ -106,7 +140,7 @@ source_account = Stellar.TxBuild.Account.new("GDC3W2X5KUTZRTQIKXM5D2I5WG5JYSEJQW
# fetch next account's sequence number from Horizon
{:ok, seq_num} = Stellar.Horizon.Accounts.fetch_next_sequence_number("GDC3W2X5KUTZRTQIKXM5D2I5WG5JYSEJQWEELVPQ5YMWZR6CA2JJ35RW")

# build a sequence number
# set the sequence number
sequence_number = Stellar.TxBuild.SequenceNumber.new(seq_num)

# set the sequence number for the transaction
Expand Down Expand Up @@ -358,3 +392,4 @@ Made with 💙 by [kommitters Open Source](https://kommit.co)
[stellar-docs-tx-envelope]: https://developers.stellar.org/docs/glossary/transactions/#transaction-envelopes
[stellar-docs-list-operations]: https://developers.stellar.org/docs/start/list-of-operations
[stellar-docs-tx-signatures]: https://developers.stellar.org/docs/glossary/multisig/#transaction-signatures
[stellar-cap-27]: https://stellar.org/protocol/cap-27
13 changes: 13 additions & 0 deletions lib/keypair.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,19 @@ defmodule Stellar.KeyPair do
@impl true
def from_secret_seed(secret), do: impl().from_secret_seed(secret)

@impl true
def from_raw_public_key(raw_public_key), do: impl().from_raw_public_key(raw_public_key)

@impl true
def from_raw_muxed_account(raw_muxed_account),
do: impl().from_raw_muxed_account(raw_muxed_account)

@impl true
def raw_public_key(public_key), do: impl().raw_public_key(public_key)

@impl true
def raw_muxed_account(public_key), do: impl().raw_muxed_account(public_key)

@impl true
def raw_secret_seed(secret), do: impl().raw_secret_seed(secret)

Expand All @@ -25,6 +35,9 @@ defmodule Stellar.KeyPair do
@impl true
def validate_public_key(public_key), do: impl().validate_public_key(public_key)

@impl true
def validate_muxed_account(public_key), do: impl().validate_muxed_account(public_key)

@impl true
def validate_secret_seed(secret), do: impl().validate_secret_seed(secret)

Expand Down
31 changes: 25 additions & 6 deletions lib/keypair/default.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ defmodule Stellar.KeyPair.Default do
{public_key, secret}
end

@impl true
def from_raw_public_key(public_key) do
StrKey.encode!(public_key, :ed25519_public_key)
end

@impl true
def from_raw_muxed_account(muxed_account) do
StrKey.encode!(muxed_account, :muxed_account)
end

@impl true
def raw_public_key(public_key) do
StrKey.decode!(public_key, :ed25519_public_key)
Expand All @@ -36,6 +46,11 @@ defmodule Stellar.KeyPair.Default do
StrKey.decode!(secret, :ed25519_secret_seed)
end

@impl true
def raw_muxed_account(muxed_account) do
StrKey.decode!(muxed_account, :muxed_account)
end

@impl true
def sign(<<payload::binary>>, <<secret::binary>>) do
raw_secret = raw_secret_seed(secret)
Expand All @@ -45,22 +60,26 @@ defmodule Stellar.KeyPair.Default do
def sign(_payload, _secret), do: {:error, :invalid_signature_payload}

@impl true
def validate_public_key(public_key) when byte_size(public_key) == 56 do
def validate_public_key(public_key) do
case StrKey.decode(public_key, :ed25519_public_key) do
{:ok, _key} -> :ok
{:error, _reason} -> {:error, :invalid_ed25519_public_key}
end
end

def validate_public_key(_public_key), do: {:error, :invalid_ed25519_public_key}
@impl true
def validate_muxed_account(muxed_account) do
case StrKey.decode(muxed_account, :muxed_account) do
{:ok, _key} -> :ok
{:error, _reason} -> {:error, :invalid_ed25519_muxed_account}
end
end

@impl true
def validate_secret_seed(secret) when byte_size(secret) == 56 do
def validate_secret_seed(secret) do
case StrKey.decode(secret, :ed25519_secret_seed) do
{:ok, _secret} -> :ok
{:ok, _key} -> :ok
{:error, _reason} -> {:error, :invalid_ed25519_secret_seed}
end
end

def validate_secret_seed(_secret), do: {:error, :invalid_ed25519_secret_seed}
end
20 changes: 12 additions & 8 deletions lib/keypair/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ defmodule Stellar.KeyPair.Spec do
@type validation :: :ok | error()

@callback random() :: {public_key(), secret_seed()}

@callback from_secret_seed(secret_seed()) :: {public_key(), secret_seed()}

@callback from_raw_public_key(binary()) :: public_key()
@callback from_raw_muxed_account(binary()) :: public_key()
@callback raw_public_key(public_key()) :: binary()

@callback raw_muxed_account(public_key()) :: binary()
@callback raw_secret_seed(public_key()) :: binary()

@callback validate_public_key(String.t()) :: validation()

@callback validate_secret_seed(String.t()) :: validation()

@callback validate_public_key(public_key()) :: validation()
@callback validate_muxed_account(public_key()) :: validation()
@callback validate_secret_seed(public_key()) :: validation()
@callback sign(binary(), secret_seed()) :: binary() | error()

@optional_callbacks from_raw_public_key: 1,
from_raw_muxed_account: 1,
validate_public_key: 1,
validate_muxed_account: 1,
validate_secret_seed: 1
end
113 changes: 98 additions & 15 deletions lib/tx_build/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,123 @@ defmodule Stellar.TxBuild.Account do
`Account` struct definition.
"""
alias Stellar.KeyPair
alias StellarBase.XDR.{CryptoKeyType, MuxedAccount, UInt256}
alias StellarBase.XDR.{CryptoKeyType, MuxedAccount, MuxedAccountMed25519, UInt64, UInt256}

@behaviour Stellar.TxBuild.XDR

@type address :: String.t()
@type account_id :: String.t()
@type validation :: {:ok, account_id()} | {:error, atom()}
@type muxed_id :: integer() | nil
@type type :: :ed25519_public_key | :ed25519_muxed_account
@type error :: {:error, atom()}
@type validation :: {:ok, any()} | error()

@type t :: %__MODULE__{account_id: account_id()}
@type t :: %__MODULE__{
address: address(),
account_id: account_id(),
muxed_id: muxed_id(),
type: type()
}

defstruct [:account_id]
defstruct [:address, :account_id, :muxed_id, :type]

@impl true
def new(account_id, opts \\ [])
def new(address, opts \\ [])

def new(account_id, _opts) do
with {:ok, account_id} <- validate_raw_account_id(account_id),
do: %__MODULE__{account_id: account_id}
def new(address, _opts) when byte_size(address) == 69 do
with {:ok, address} <- validate_muxed_address(address),
{:ok, {account_id, muxed_id}} <- parse_muxed_address(address) do
%__MODULE__{
address: address,
account_id: account_id,
muxed_id: muxed_id,
type: :ed25519_muxed_account
}
end
end

def new(address, _opts) when byte_size(address) == 56 do
case validate_public_key(address) do
{:ok, account_id} ->
%__MODULE__{
address: account_id,
account_id: account_id,
type: :ed25519_public_key
}

error ->
error
end
end

def new(_address, _opts), do: {:error, :invalid_ed25519_public_key}

@impl true
def to_xdr(%__MODULE__{account_id: account_id, muxed_id: muxed_id, type: :ed25519_muxed_account}) do
type = CryptoKeyType.new(:KEY_TYPE_MUXED_ED25519)
ed25519_public_key_xdr = ed25519_public_key_xdr(account_id)

muxed_id
|> UInt64.new()
|> MuxedAccountMed25519.new(ed25519_public_key_xdr)
|> MuxedAccount.new(type)
end

def to_xdr(%__MODULE__{account_id: account_id}) do
type = CryptoKeyType.new(:KEY_TYPE_ED25519)
ed25519_public_key_xdr = ed25519_public_key_xdr(account_id)

MuxedAccount.new(ed25519_public_key_xdr, type)
end

@spec create_muxed(account_id :: account_id(), muxed_id :: muxed_id()) :: t()
def create_muxed(account_id, muxed_id)
when byte_size(account_id) == 56 and is_integer(muxed_id) do
account_id
|> KeyPair.raw_public_key()
|> UInt256.new()
|> MuxedAccount.new(type)
|> Kernel.<>(<<muxed_id::big-unsigned-integer-size(64)>>)
|> KeyPair.from_raw_muxed_account()
|> new()
end

@spec validate_raw_account_id(account_id :: String.t()) :: validation()
defp validate_raw_account_id(account_id) do
case KeyPair.validate_public_key(account_id) do
:ok -> {:ok, account_id}
_error -> {:error, :invalid_account_id}
def create_muxed(_account_id, _id), do: {:error, :invalid_muxed_account}

@spec validate_muxed_address(address :: address()) :: validation()
defp validate_muxed_address(address) do
case KeyPair.validate_muxed_account(address) do
:ok -> {:ok, address}
error -> error
end
end

@spec validate_public_key(address :: address()) :: validation()
defp validate_public_key(address) do
case KeyPair.validate_public_key(address) do
:ok -> {:ok, address}
error -> error
end
end

@spec parse_muxed_address(address :: address()) :: {:ok, {account_id(), muxed_id()}} | error()
defp parse_muxed_address(address) do
address
|> KeyPair.raw_muxed_account()
|> encode_muxed_address()
end

@spec encode_muxed_address(decoded_address :: binary()) ::
{:ok, {account_id(), muxed_id()}} | error()
defp encode_muxed_address(<<decoded::binary-size(32), muxed_id::big-unsigned-integer-size(64)>>) do
ed25519_key = KeyPair.from_raw_public_key(decoded)
{:ok, {ed25519_key, muxed_id}}
end

defp encode_muxed_address(_muxed_account), do: {:error, :invalid_muxed_address}

@spec ed25519_public_key_xdr(account_id :: account_id()) :: UInt256.t()
defp ed25519_public_key_xdr(account_id) do
account_id
|> KeyPair.raw_public_key()
|> UInt256.new()
end
end
14 changes: 4 additions & 10 deletions lib/tx_build/account_id.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ defmodule Stellar.TxBuild.AccountID do
def new(account_id, opts \\ [])

def new(account_id, _opts) do
with {:ok, account_id} <- validate_raw_account_id(account_id),
do: %__MODULE__{account_id: account_id}
case KeyPair.validate_public_key(account_id) do
:ok -> %__MODULE__{account_id: account_id}
error -> error
end
end

@impl true
Expand All @@ -32,12 +34,4 @@ defmodule Stellar.TxBuild.AccountID do
|> PublicKey.new(type)
|> AccountID.new()
end

@spec validate_raw_account_id(account_id :: String.t()) :: validation()
defp validate_raw_account_id(account_id) do
case KeyPair.validate_public_key(account_id) do
:ok -> {:ok, account_id}
_error -> {:error, :invalid_account_id}
end
end
end
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Stellar.MixProject do
use Mix.Project

@github_url "https://github.com/kommitters/stellar_sdk"
@version "0.2.0"
@version "0.3.0"

def project do
[
Expand Down Expand Up @@ -37,7 +37,7 @@ defmodule Stellar.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:stellar_base, "~> 0.3.0"},
{:stellar_base, "~> 0.5.0"},
{:ed25519, "~> 1.3"},
{:hackney, "~> 1.17", optional: true},
{:jason, "~> 1.0", optional: true},
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"},
"crc": {:hex, :crc, "0.10.2", "93ee6788904735d4d93f59a1e80860e4c9aa44e8d2ff7c69857eb62757454137", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "6b931cfb5e7d20c3c4113adab460f29ee5a50a36b397edd81c9bede2bbdb505c"},
"crc": {:hex, :crc, "0.10.4", "06f5f54e2ec2954968703dcd37d7a4c65cee7a5305c48a23c509dc20a5469d4f", [:mix, :rebar3], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "90bdd5b5ac883f0b1860692324ed3f53fdaa6f1e8483771873fea07e71def91d"},
"credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"},
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"},
Expand All @@ -24,6 +24,6 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"stellar_base": {:hex, :stellar_base, "0.3.0", "21e0253044ba2bcb86f1af92e2847543b42c40cac0863318a22d5a252814d799", [:mix], [{:crc, "~> 0.10.0", [hex: :crc, repo: "hexpm", optional: false]}, {:elixir_xdr, "~> 0.2.0", [hex: :elixir_xdr, repo: "hexpm", optional: false]}], "hexpm", "9f53a22277842a2fc70d5544944bd04a8cd9e00b4cfb5d627887cb30313eff5d"},
"stellar_base": {:hex, :stellar_base, "0.5.0", "41dab1d99b86f7ee8a86d07d1ddc59234f3be1df6a327479a7225b347425f546", [:mix], [{:crc, "~> 0.10.0", [hex: :crc, repo: "hexpm", optional: false]}, {:elixir_xdr, "~> 0.2.0", [hex: :elixir_xdr, repo: "hexpm", optional: false]}], "hexpm", "fa9f5c9bcc8607f2b80b49791541fab3835442f89b98f848bd86744873862cdc"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
}
Loading

0 comments on commit 6d0afe3

Please sign in to comment.