Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BumpFootprintExpiration operation #323

Merged
merged 5 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
* [Upload Contract Code (WASM)](/docs/examples/soroban/upload_contract_code.md)
* [Create Contract](/docs/examples/soroban/create_contract.md)
* [Invoke Contract Function](/docs/examples/soroban/invoke_contract_functions.md)
* [Bump Footprint Expiration](/docs/examples/soroban/bump_footprint_expiration.md)
96 changes: 96 additions & 0 deletions docs/examples/soroban/bump_footprint_expiration.md
Odraxs marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Bump Footprint Expiration
The `BumpFootprintExpirationOp` operation is used to extend a contract data entry's lifetime.

A contract instance, wasm hash, and data storage entry (persistent/instance/temporary) can expire, so you can use this bump operation to extend its lifetime.
Read more about it:
- https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#bumpfootprintexpirationop
- https://docs.rs/soroban-sdk/latest/soroban_sdk/storage/struct.Storage.html

In this example, we will bump the contract instance of an already deployed contract in the network, adding 1000 ledgers to it.

> **Warning**
> Please note that Soroban is still under development, so breaking changes may occur.

> **Note**
> All this actions require to use `simulateTransaction` and `sendTransaction` RPC endpoints when specified in the code comments to achieve the bump footprint expiration.

```elixir
alias Stellar.TxBuild.{
Account,
BaseFee,
BumpFootprintExpiration,
LedgerKey,
SCAddress,
SCVal,
SequenceNumber,
Signature,
SorobanResources,
SorobanTransactionData
}

alias Stellar.KeyPair

contract_address = "CAMGSYINVVL6WP3Q5WPNL7FS4GZP37TWV7MKIRQF5QMYLK3N2SW4P3RC"
contract_sc_address = SCAddress.new(contract_address)
key = SCVal.new(ledger_key_contract_instance: nil)

keypair =
{public_key, _secret} =
Stellar.KeyPair.from_secret_seed("SCAVFA3PI3MJLTQNMXOUNBSEUOSY66YMG3T2KCQKLQBENNVLVKNPV3EK")

contract_data =
LedgerKey.new(
{:contract_data,
[
contract: contract_sc_address,
key: key,
durability: :persistent,
body_type: :data_entry
]}
)

hash= StellarBase.StrKey.decode!(contract_address, :contract)
contract_code = LedgerKey.new({:contract_code, [hash: hash, body_type: :data_entry]})
footprint = LedgerFootprint.new(read_only: [contract_data, contract_code])

soroban_data =
[
footprint: footprint,
instructions: 0,
read_bytes: 0,
write_bytes: 0,
extended_meta_data_size_bytes: 0
]
|> SorobanResources.new()
|> (&SorobanTransactionData.new(resources: &1, refundable_fee: 0)).()
|> SorobanTransactionData.to_xdr()

source_account = Account.new(public_key)
{:ok, seq_num} = Accounts.fetch_next_sequence_number(public_key)
sequence_number = SequenceNumber.new(seq_num)
signature = Signature.new(keypair)
bump_footprint_op = BumpFootprintExpiration.new(ledgers_to_expire: 1000)

# Use this XDR to simulate the transaction and get the soroban_data and min_resource_fee
source_account
|> Stellar.TxBuild.new(sequence_number: sequence_number)
|> Stellar.TxBuild.add_operation(bump_footprint_op)
|> Stellar.TxBuild.envelope()

# Simulate Transaction
soroban_data =
"AAAAAAAAAAIAAAAGAAAAARhpYQ2tV+s/cO2e1fyy4bL9/nav2KRGBewZhatt1K3HAAAAFAAAAAEAAAAAAAAABxhpYQ2tV+s/cO2e1fyy4bL9/nav2KRGBewZhatt1K3HAAAAAAAAAAAAAAAAAAABLAAAAAAAAAJYAAAAAAAAAHY="

min_resource_fee = 11_516
fee = BaseFee.new(min_resource_fee + 100)

# Use the XDR generated here to send it to the futurenet
source_account
|> Stellar.TxBuild.new(sequence_number: sequence_number)
|> Stellar.TxBuild.add_operation(bump_footprint_op)
|> Stellar.TxBuild.set_soroban_data(soroban_data)
|> Stellar.TxBuild.set_base_fee(fee)
|> Stellar.TxBuild.sign(signature)
|> Stellar.TxBuild.envelope()

```
55 changes: 55 additions & 0 deletions lib/tx_build/bump_footprint_expiration.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
defmodule Stellar.TxBuild.BumpFootprintExpiration do
@moduledoc """
`BumpFootprintExpiration` struct definition.
"""
import Stellar.TxBuild.Validations, only: [validate_optional_account: 1]

alias Stellar.TxBuild.OptionalAccount

alias StellarBase.XDR.{
ExtensionPoint,
OperationBody,
OperationType,
Operations.BumpFootprintExpiration,
UInt32,
Void
}

@behaviour Stellar.TxBuild.XDR

@type ledgers_to_expire :: integer()

@type t :: %__MODULE__{
ledgers_to_expire: ledgers_to_expire(),
source_account: OptionalAccount.t()
}

defstruct [:ledgers_to_expire, :source_account]

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

def new(args, _opts) when is_list(args) do
ledgers_to_expire = Keyword.get(args, :ledgers_to_expire, 1)
source_account = Keyword.get(args, :source_account)

with {:ok, source_account} <- validate_optional_account({:source_account, source_account}) do
%__MODULE__{ledgers_to_expire: ledgers_to_expire, source_account: source_account}
end
end

def new(_value, _opts), do: {:error, :invalid_bump_footprint_op}

@impl true
def to_xdr(%__MODULE__{ledgers_to_expire: ledgers_to_expire}) do
op_type = OperationType.new(:BUMP_FOOTPRINT_EXPIRATION)
ledgers_to_expire = UInt32.new(ledgers_to_expire)

Void.new()
|> ExtensionPoint.new(0)
|> BumpFootprintExpiration.new(ledgers_to_expire)
|> OperationBody.new(op_type)
end

def to_xdr(_struct), do: {:error, :invalid_struct}
end
60 changes: 60 additions & 0 deletions lib/tx_build/ledger/contract_code.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule Stellar.TxBuild.Ledger.ContractCode do
@moduledoc """
`ContractCode` struct definition.
"""

alias StellarBase.XDR.{ContractEntryBodyType, Hash, LedgerKeyContractCode}

@behaviour Stellar.TxBuild.XDR

@type error :: {:error, atom()}
@type validation :: {:ok, any()} | error()
@type hash :: String.t()
@type body_type :: :data_entry | :expiration_ext

@type t :: %__MODULE__{
hash: hash(),
body_type: body_type()
}

defstruct [:hash, :body_type]

@allowed_body_types ~w(data_entry expiration_ext)a

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

def new(
[
{:hash, hash},
{:body_type, body_type}
],
_opts
)
when is_binary(hash) and body_type in @allowed_body_types do
%__MODULE__{
hash: hash,
body_type: body_type
}
end

def new(_value, _opts), do: {:error, :invalid_ledger_key_args}

@impl true
def to_xdr(%__MODULE__{
hash: hash,
body_type: body_type
}) do
body_type = body_type_to_xdr(body_type)

hash
|> Hash.new()
|> LedgerKeyContractCode.new(body_type)
end

def to_xdr(_struct), do: {:error, :invalid_struct}

@spec body_type_to_xdr(atom()) :: ContractEntryBodyType.t()
defp body_type_to_xdr(:data_entry), do: ContractEntryBodyType.new(:DATA_ENTRY)
defp body_type_to_xdr(:expiration_ext), do: ContractEntryBodyType.new(:EXPIRATION_EXTENSION)
end
90 changes: 90 additions & 0 deletions lib/tx_build/ledger/contract_data.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
defmodule Stellar.TxBuild.Ledger.ContractData do
@moduledoc """
`ContractData` struct definition.
"""

import Stellar.TxBuild.Validations,
only: [validate_address: 1]

alias Stellar.TxBuild.{SCAddress, SCVal}
alias StellarBase.XDR.{ContractEntryBodyType, ContractDataDurability, LedgerKeyContractData}

@behaviour Stellar.TxBuild.XDR

@type error :: {:error, atom()}
@type validation :: {:ok, any()} | error()
@type contract :: SCAddress.t()
@type key :: SCVal.t()
@type durability :: :temporary | :persistent
@type body_type :: :data_entry | :expiration_ext

@type t :: %__MODULE__{
contract: contract(),
key: key(),
durability: durability(),
body_type: body_type()
}

defstruct [:contract, :key, :durability, :body_type]

@allowed_durabilities ~w(temporary persistent)a
@allowed_body_types ~w(data_entry expiration_ext)a

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

def new(
[
{:contract, contract},
{:key, key},
{:durability, durability},
{:body_type, body_type}
],
_opts
)
when durability in @allowed_durabilities and body_type in @allowed_body_types do
with {:ok, contract} <- validate_address(contract),
{:ok, key} <- validate_sc_val_ledger_instance(key) do
%__MODULE__{
contract: contract,
key: key,
durability: durability,
body_type: body_type
}
end
end

def new(_value, _opts), do: {:error, :invalid_ledger_key_args}

@impl true
def to_xdr(%__MODULE__{
contract: contract,
key: key,
durability: durability,
body_type: body_type
}) do
key = SCVal.to_xdr(key)
durability = durability_to_xdr(durability)
body_type = body_type_to_xdr(body_type)

contract
|> SCAddress.to_xdr()
|> LedgerKeyContractData.new(key, durability, body_type)
end

def to_xdr(_struct), do: {:error, :invalid_struct}

@spec validate_sc_val_ledger_instance(key :: key()) :: validation()
defp validate_sc_val_ledger_instance(%SCVal{type: :ledger_key_contract_instance} = key),
do: {:ok, key}

defp validate_sc_val_ledger_instance(_key), do: {:error, :invalid_key}

@spec durability_to_xdr(atom()) :: ContractDataDurability.t()
defp durability_to_xdr(:temporary), do: ContractDataDurability.new(:TEMPORARY)
defp durability_to_xdr(:persistent), do: ContractDataDurability.new(:PERSISTENT)

@spec body_type_to_xdr(atom()) :: ContractEntryBodyType.t()
defp body_type_to_xdr(:data_entry), do: ContractEntryBodyType.new(:DATA_ENTRY)
defp body_type_to_xdr(:expiration_ext), do: ContractEntryBodyType.new(:EXPIRATION_EXTENSION)
end
71 changes: 71 additions & 0 deletions lib/tx_build/ledger_footprint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule Stellar.TxBuild.LedgerFootprint do
@moduledoc """
`LedgerFootprint` struct definition.
"""

alias Stellar.TxBuild.LedgerKey
alias StellarBase.XDR.{LedgerFootprint, LedgerKeyList}

@behaviour Stellar.TxBuild.XDR

@type error :: {:error, atom()}
@type validation :: {:ok, any()} | error()
@type ledger_key :: LedgerKey.t()
@type ledger_keys :: list(ledger_key)

@type t :: %__MODULE__{
read_only: ledger_keys(),
read_write: ledger_keys()
}

defstruct [:read_only, :read_write]

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

def new(args, _opts) when is_list(args) do
read_only = Keyword.get(args, :read_only, [])
read_write = Keyword.get(args, :read_write, [])

with {:ok, read_only} <- validate_ledger_keys(read_only),
{:ok, read_write} <- validate_ledger_keys(read_write) do
%__MODULE__{
read_only: read_only,
read_write: read_write
}
end
end

def new(_value, _opts), do: {:error, :invalid_ledger_footprint}

@impl true
def to_xdr(%__MODULE__{read_only: read_only, read_write: read_write}) do
read_write = to_ledger_key_list(read_write)

read_only
|> to_ledger_key_list()
|> LedgerFootprint.new(read_write)
end

def to_xdr(_struct), do: {:error, :invalid_struct}

@spec validate_ledger_keys(keys :: ledger_keys()) :: validation()
defp validate_ledger_keys(keys) when is_list(keys) do
if Enum.all?(keys, &validate_ledger_key/1),
do: {:ok, keys},
else: {:error, :invalid_ledger_keys}
end

defp validate_ledger_keys(_keys), do: {:error, :invalid_ledger_keys}

@spec validate_ledger_key(ledger_key()) :: boolean()
defp validate_ledger_key(%LedgerKey{}), do: true
defp validate_ledger_key(_key), do: false

@spec to_ledger_key_list(keys :: ledger_keys()) :: LedgerKeyList.t()
defp to_ledger_key_list(keys) do
keys
|> Enum.map(&LedgerKey.to_xdr/1)
|> LedgerKeyList.new()
end
end
Loading