defmodule Icon do
@moduledoc """
`Icon` is a library for interacting with the interoperable decentralized
aggregator network [ICON 2.0](https://icon.foundation).
"""
alias Icon.RPC.{Identity, Request}
alias Icon.Schema
alias Icon.Schema.{
Error,
Types.Address,
Types.BinaryData,
Types.Block,
Types.Hash,
Types.Loop,
Types.SCORE,
Types.Transaction
}
@doc """
Gets block by `hash_or_height`. If hash or height are not provided, it will
retrieve the latest block.
## Example
- Get latest block:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_block(identity)
{
:ok,
%Icon.Schema.Types.Block{
block_hash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
confirmed_transaction_list: [
%Icon.Schema.Types.Transaction{
data: %{
result: %{
coveredByFee: 0,
coveredByOverIssuedICX: 12_000,
issue: 0
}
},
dataType: :base,
timestamp: ~U[2022-01-22 11:06:21.258886Z],
txHash: "0x75e553dcd57853e6c96428c4fede49209a3055fc905db757baa470c1e94f736d",
version: 3
}
],
height: 3_153_751,
merkle_tree_root_hash: "0xce5aa42a762ee88a32fc2a792dfb5975858a71a8abf4ec51fb1218e3b827aa01",
peer_id: "hxb97c82a5577a0a436f51a41421ad2d3b28da3f25",
prev_block_hash: "0xfe8138afd24512cc0e9f4da8df350300a759a480f15c8a00b04b2d753ea62ac3",
signature: nil,
time_stamp: ~U[2022-01-22 11:06:21.258886Z],
version: "2.0"
}
}
```
- Get block by height:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_block(identity, 3_153_751)
{
:ok,
%Icon.Schema.Types.Block{
block_hash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
confirmed_transaction_list: [
%Icon.Schema.Types.Transaction{
data: %{
result: %{
coveredByFee: 0,
coveredByOverIssuedICX: 12_000,
issue: 0
}
},
dataType: :base,
timestamp: ~U[2022-01-22 11:06:21.258886Z],
txHash: "0x75e553dcd57853e6c96428c4fede49209a3055fc905db757baa470c1e94f736d",
version: 3
}
],
height: 3_153_751,
merkle_tree_root_hash: "0xce5aa42a762ee88a32fc2a792dfb5975858a71a8abf4ec51fb1218e3b827aa01",
peer_id: "hxb97c82a5577a0a436f51a41421ad2d3b28da3f25",
prev_block_hash: "0xfe8138afd24512cc0e9f4da8df350300a759a480f15c8a00b04b2d753ea62ac3",
signature: nil,
time_stamp: ~U[2022-01-22 11:06:21.258886Z],
version: "2.0"
}
}
```
- Get block by hash:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_block(identity, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b")
{
:ok,
%Icon.Schema.Types.Block{
block_hash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
confirmed_transaction_list: [
%Icon.Schema.Types.Transaction{
data: %{
result: %{
coveredByFee: 0,
coveredByOverIssuedICX: 12_000,
issue: 0
}
},
dataType: :base,
timestamp: ~U[2022-01-22 11:06:21.258886Z],
txHash: "0x75e553dcd57853e6c96428c4fede49209a3055fc905db757baa470c1e94f736d",
version: 3
}
],
height: 3_153_751,
merkle_tree_root_hash: "0xce5aa42a762ee88a32fc2a792dfb5975858a71a8abf4ec51fb1218e3b827aa01",
peer_id: "hxb97c82a5577a0a436f51a41421ad2d3b28da3f25",
prev_block_hash: "0xfe8138afd24512cc0e9f4da8df350300a759a480f15c8a00b04b2d753ea62ac3",
signature: nil,
time_stamp: ~U[2022-01-22 11:06:21.258886Z],
version: "2.0"
}
}
```
"""
@spec get_block(Identity.t()) ::
{:ok, Block.t()}
| {:error, Error.t()}
@spec get_block(Identity.t(), nil | pos_integer() | Hash.t()) ::
{:ok, Block.t()}
| {:error, Error.t()}
def get_block(identity, height_or_hash \\ nil)
def get_block(identity, nil) do
with {:ok, request} <- Request.Goloop.get_last_block(identity),
{:ok, response} <- Request.send(request) do
load_block(response)
end
end
def get_block(identity, height) when is_integer(height) and height > 0 do
with {:ok, request} <- Request.Goloop.get_block_by_height(identity, height),
{:ok, response} <- Request.send(request) do
load_block(response)
end
end
def get_block(identity, hash) do
with {:ok, request} <- Request.Goloop.get_block_by_hash(identity, hash),
{:ok, response} <- Request.send(request) do
load_block(response)
end
end
@doc """
Calls a readonly SCORE `method` (no transaction).
The `identity` should be created using a valid `private_key`, otherwise the
call cannot be executed.
Options:
- `call_schema` - Schema to validate the `params`. When no schema is provided,
the default schema `:any` will be used instead.
- `response_schema` - Schema for transforming the incoming values. When no
schema is provided, the default schema `:any` will be used instead.
## Example
- Calling the method `getBalance` without parameters:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.call(
...> identity,
...> "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
...> "getBalance",
...> )
{:ok, "0x2a"}
```
- Calling the method `getBalance` with an address as parameter without type
conversion:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.call(
...> identity,
...> "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
...> "getBalance",
...> %{address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"},
...> )
{:ok, "0x2a"}
```
- Calling the method `getBalance` with an address as parameter with type
conversion using a `call_schema` (recommended):
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.call(
...> identity,
...> "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
...> "getBalance",
...> %{address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"},
...> call_schema: %{address: {:address, required: true}},
...> response_schema: :loop
...> )
{:ok, 42}
```
"""
@spec call(Identity.t(), SCORE.t(), binary()) ::
{:ok, any()}
| {:error, Error.t()}
@spec call(Identity.t(), SCORE.t(), binary(), nil | map() | keyword()) ::
{:ok, any()}
| {:error, Error.t()}
@spec call(
Identity.t(),
SCORE.t(),
binary(),
nil | map() | keyword(),
keyword()
) ::
{:ok, any()}
| {:error, Error.t()}
def call(identity, score_address, method, params \\ nil, options \\ [])
def call(%Identity{} = identity, to, method, params, options) do
options =
case options[:call_schema] do
nil ->
options
value ->
options
|> Keyword.delete(:call_schema)
|> Keyword.put(:schema, value)
end
with {:ok, request} <-
Request.Goloop.call(identity, to, method, params, options),
{:ok, response} <- Request.send(request) do
load_call_response(response, options)
end
end
@doc """
Gets the balance of an EOA or SCORE `address`. If the `address` is not provided,
it uses the one in the `identity`. The balance is returned in loop
(1 ICX = 10¹⁸ loop).
## Examples
- Requesting the balance of the loaded identity:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.get_balance(identity)
{:ok, 2_045_995_000_000_000_000_000}
```
- Requesting the balance of a wallet:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_balance(identity, "hxbe258ceb872e08851f1f59694dac2558708ece11")
{:ok, 0}
```
"""
@spec get_balance(Identity.t()) :: {:ok, Loop.t()} | {:error, Error.t()}
@spec get_balance(Identity.t(), nil | Address.t()) ::
{:ok, Loop.t()}
| {:error, Error.t()}
def get_balance(identity, address \\ nil)
def get_balance(%Identity{} = identity, address) do
with {:ok, request} <- Request.Goloop.get_balance(identity, address),
{:ok, response} <- Request.send(request),
:error <- Loop.load(response) do
reason =
Error.new(
reason: :server_error,
message: "cannot cast balance to loop"
)
{:error, reason}
end
end
@doc """
Gets SCORE API.
## Example
- Gets the API of a SCORE:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_score_api(identity, "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32")
{
:ok,
[
%{
"type" => "function",
"name" => "balanceOf",
"inputs" => [
%{
"name" => "_owner",
"type" => "Address"
}
],
"outputs" => [
%{
"type" => "int"
}
],
"readonly" => "0x1"
},
...
]
}
```
## API Entries
Each member of the list will have the following keys:
Key | Description
:--------- | :----------
`type` | Either `function`, `fallback` or `eventlog`.
`name` | Name of the function or the event log.
`inputs` | A list of parameters the function or the event receives.
`outputs` | A list of values a function returns.
`readonly` | Whether the function call can be done without a transaction or not.
`payable` | Whether the function can be paid or not.
> Note: Both `readonly` and `payable` will be returned in the ICON 2.0
> representation or a boolean value e.g. `0x1` for `true`.
Each input will have the following keys:
Key | Description
:-------- | :----------
`name` | Parameter name.
`type` | Parameter type. Either `int`, `str`, `bytes`, `bool` or `Address`.
`indexed` | (Only for event logs) if the parameter is indexed or not.
> Note: `indexed` will be returned in the ICON 2.0 representation of a boolean
> e.g. `0x1` for `true`.
Each output will have the following keys:
Key | Description
:----- | :----------
`type` | Result type. Either `int`, `str`, `bytes`, `bool`, `Address`, `dict` or `list`.
"""
@spec get_score_api(Identity.t(), SCORE.t()) ::
{:ok, list()}
| {:error, Error.t()}
def get_score_api(identity, score_address)
def get_score_api(%Identity{} = identity, score_address) do
with {:ok, request} <- Request.Goloop.get_score_api(identity, score_address) do
Request.send(request)
end
end
@doc """
Gets the ICX's total supply in loop (1 ICX = 10¹⁸ loop).
## Examples
- Requesting ICX's total supply:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.total_supply(identity)
{:ok, 1_300_163_572_018_865_530_968_203_250}
```
"""
@spec get_total_supply(Identity.t()) :: {:ok, Loop.t()} | {:error, Error.t()}
def get_total_supply(identity)
def get_total_supply(%Identity{} = identity) do
with {:ok, request} <- Request.Goloop.get_total_supply(identity),
{:ok, response} <- Request.send(request),
:error <- Loop.load(response) do
reason =
Error.new(
reason: :server_error,
message: "cannot cast total supply to loop"
)
{:error, reason}
end
end
@doc """
Gets a transaction result by `hash`.
Options:
- `timeout` - Timeout in milliseconds for waiting for the result of the
transaction in case it's pending.
## Example
- Requesting a successful transaction result by `hash`:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_transaction_result(identity, ""0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25"")
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0xd6e8ed8035b38a5c09de59df101c7e6258e6d7e0690d3c6c6093045a5550bb83",
blockHeight: 45162694,
cumulativeStepUsed: 0,
eventLogs: [
%Icon.Schema.Types.EventLog{
data: ["{\\"method\\": \\"_swap\\", \\"params\\": {\\"toToken\\": \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\", \\"minimumReceive\\": \\"1000020000000000000000\\", \\"path\\": [\\"cx2609b924e33ef00b648a409245c7ea394c467824\\", \\"cxf61cd5a45dc9f91c15aa65831a30a90d59a09619\\", \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\"]}}"],
header: "Transfer(Address,Address,int,bytes)",
indexed: [
"hx948b9727f426ae7789741da8c796807f78ba137f",
"cx21e94c08c03daee80c25d8ee3ea22a20786ec231",
1000020000000000000000
],
name: "Transfer",
score_address: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb"
},
...
]
failure: nil,
logsBloom: <<0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 16, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 1, 0, 1, 0, 0, 16, 0, 128,
...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12500000000,
stepUsed: 13816635,
to: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb",
txHash: "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25",
txIndex: 2
}
}
```
"""
@spec get_transaction_result(Identity.t(), Hash.t()) ::
{:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec get_transaction_result(Identity.t(), Hash.t(), keyword()) ::
{:ok, Transaction.Result.t()}
| {:error, Error.t()}
def get_transaction_result(identity, tx_hash, options \\ [])
def get_transaction_result(%Identity{} = identity, hash, options) do
with {:ok, request} <-
Request.Goloop.get_transaction_result(identity, hash, options),
{:ok, response} <- Request.send(request) do
load_transaction_result(response)
end
end
@doc """
Gets a transaction by `hash`.
## Example
- Requesting a successful transaction by `hash`:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> Icon.get_transaction_by_hash(identity, "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25")
{
:ok,
%Icon.Schema.Types.Transaction{
blockHash: "0xd6e8ed8035b38a5c09de59df101c7e6258e6d7e0690d3c6c6093045a5550bb83",
blockHeight: 45162694,
data: %{
method: "transfer",
params: %{
"_data" => "0x7b226d6574686f64223a20225f73776170222c2022706172616d73223a207b22746f546f6b656e223a2022637838386664376466376464666638326637636337333563383731646335313938333863623233356262222c20226d696e696d756d52656365697665223a202231303030303230303030303030303030303030303030222c202270617468223a205b22637832363039623932346533336566303062363438613430393234356337656133393463343637383234222c2022637866363163643561343564633966393163313561613635383331613330613930643539613039363139222c2022637838386664376466376464666638326637636337333563383731646335313938333863623233356262225d7d7d",
"_to" => "cx21e94c08c03daee80c25d8ee3ea22a20786ec231",
"_value" => "0x363610bbaabe220000"
}
},
dataType: :call,
from: "hx948b9727f426ae7789741da8c796807f78ba137f",
nid: 1,
nonce: 223,
signature: "TTfXvXZ3NG53R2tx9D69fMvmHW8mIIWWEDZnNfOgGG1BOeGYSYzV37PWCi7ryXKKAc7e80ue937yrull8hoZxgE=",
stepLimit: 50000000,
timestamp: ~U[2022-01-22 06:48:51.250054Z],
to: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb",
txHash: "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25",
txIndex: 2,
value: 0,
version: 3
}
}
```
The `params` key cannot be decoded beforehand, so we need to use a schema to
retrieve the Elixir values. Using the previous example, we would do something
like this:
```elixir
iex> identity = Icon.RPC.Identity.new()
iex> {:ok, tx} = Icon.get_transaction_by_hash(identity, "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25")
iex> schema = %{
...> _data: :binary_data,
...> _to: :address,
...> _value: :loop
...> }
iex> {:ok, decoded_params} = (
...> schema
...> |> Icon.Schema.generate()
...> |> Icon.Schema.new(tx.data.params)
...> |> Icon.Schema.load()
...> |> Icon.Schema.apply()
...> )
iex> put_in(tx, [:data, :params], decoded_params)
%Icon.Schema.Types.Transaction{
blockHash: "0xd6e8ed8035b38a5c09de59df101c7e6258e6d7e0690d3c6c6093045a5550bb83",
blockHeight: 45162694,
data: %{
method: "transfer",
params: %{
_data: "{\\"method\\": \\"_swap\\", \\"params\\": {\\"toToken\\": \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\", \\"minimumReceive\\": \\"1000020000000000000000\\", \\"path\\": [\\"cx2609b924e33ef00b648a409245c7ea394c467824\\", \\"cxf61cd5a45dc9f91c15aa65831a30a90d59a09619\\", \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\"]}}",
_to: "cx21e94c08c03daee80c25d8ee3ea22a20786ec231",
_value: 1000020000000000000000
}
},
dataType: :call,
from: "hx948b9727f426ae7789741da8c796807f78ba137f",
nid: 1,
nonce: 223,
signature: "TTfXvXZ3NG53R2tx9D69fMvmHW8mIIWWEDZnNfOgGG1BOeGYSYzV37PWCi7ryXKKAc7e80ue937yrull8hoZxgE=",
stepLimit: 50000000,
timestamp: ~U[2022-01-22 06:48:51.250054Z],
to: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb",
txHash: "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25",
txIndex: 2,
value: 0,
version: 3
}
```
For more information about schemas see `Icon.Schema` module.
"""
@spec get_transaction_by_hash(Identity.t(), Hash.t()) ::
{:ok, Transaction.t()}
| {:error, Error.t()}
def get_transaction_by_hash(identity, hash)
def get_transaction_by_hash(%Identity{} = identity, hash) do
with {:ok, request} <-
Request.Goloop.get_transaction_by_hash(identity, hash),
{:ok, response} <- Request.send(request) do
load_transaction(response)
end
end
@doc """
Transfers an ICX `amount` to a `recipient`.
The `identity` should be created using a valid `private_key`, otherwise the
transfer cannot be executed.
Options:
- `timeout` - Time in milliseconds to wait for the transfer result.
- `params` - Extra transaction parameters for overriding the defaults.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Examples
- Transfer `1.00` ICX to another wallet:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
iex> amount = 1_000_000_000_000_000_000 # 1 ICX in loop
iex> Icon.transfer(identity, recipient, amount)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Transfer `1.00` ICX to another wallet and wait 5 seconds for the result:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
iex> amount = 1_000_000_000_000_000_000 # 1 ICX in loop
iex> Icon.transfer(identity, recipient, amount, timeout: 5_000)
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "hx2e243ad926ac48d15156756fce28314357d49d83",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
"""
@spec transfer(Identity.t(), Address.t(), Loop.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec transfer(Identity.t(), Address.t(), Loop.t(), keyword()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def transfer(identity, recipient, amount, options \\ [])
def transfer(%Identity{} = identity, to, value, options) do
with {:ok, request} <-
Request.Goloop.transfer(identity, to, value, options) do
send_transaction(request)
end
end
@doc """
Send a signed `message` to a `recipient`.
The `identity` should be created using a valid `private_key`, otherwise the
message cannot be sent.
Options:
- `timeout` - Time in milliseconds to wait for the transfer result.
- `params` - Extra transaction parameters for overriding the defaults.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Examples
- Send a message to another wallet:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
iex> Icon.send_message(identity, recipient, "Hello!")
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Send a message to another wallet and wait 5 seconds for the result:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
iex> Icon.send_message(identity, recipient, "Hello!", timeout: 5_000)
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "hx2e243ad926ac48d15156756fce28314357d49d83",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
"""
@spec send_message(Identity.t(), Schema.Types.Address.t(), binary()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec send_message(
Identity.t(),
Schema.Types.Address.t(),
binary(),
keyword()
) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def send_message(identity, recipient, message, options \\ [])
def send_message(%Identity{} = identity, to, message, options) do
with {:ok, request} <-
Request.Goloop.send_message(identity, to, message, options) do
send_transaction(request)
end
end
@doc """
Calls a `method` in a `score` in a signed transaction.
Options:
- `call_schema` - Method parameters schema.
- `timeout` - Time in milliseconds to wait for the transaction result.
- `params` - Extra transaction parameters for overriding the defaults.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Call Schema
The `call_schema` option gives defines the types of the `call_params`. This is
required for `method` calls with parameters, because ICON has a different
type representation than Elixir e.g. let's say we want to call the method
`transfer(from: Address, to: Address, amount: int)` for transferring an
`amount` of tokens `from` one EOA address `to` another. Then the schema would
look like this:
```elixir
%{
from: {:eoa_address, required: true},
to: {:eoa_address, required: true},
amount: {:loop, required: true}
}
```
Then, while doing the actual transaction call, the schema will help with the
conversion of the `call_params`. So, something like the following:
```elixir
%{
from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
to: "hx2e243ad926ac48d15156756fce28314357d49d83",
amount: 1_000_000_000_000_000_000
}
```
will be converted to the following when communicating with the node:
```json
{
"from": "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
"to": "hx2e243ad926ac48d15156756fce28314357d49d83",
"amount": "0xde0b6b3a7640000"
}
```
## Examples
Given the example method used in the previous section, we can call it as
follows:
- Transfer tokens from one wallet to another:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.transaction_call(
...> identity,
...> "cx2e243ad926ac48d15156756fce28314357d49d83",
...> "transfer",
...> %{
...> from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
...> to: "hx2e243ad926ac48d15156756fce28314357d49d83",
...> amount: 1_000_000_000_000_000_000
...> },
...> call_schema: %{
...> from: {:eoa_address, required: true},
...> to: {:eoa_address, required: true},
...> amount: {:loop, required: true}
...> }
...> )
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Transfer token from one wallet to another and wait 5 second for the result:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.transaction_call(
...> identity,
...> "cx2e243ad926ac48d15156756fce28314357d49d83",
...> "transfer",
...> %{
...> from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
...> to: "hx2e243ad926ac48d15156756fce28314357d49d83",
...> amount: 1_000_000_000_000_000_000
...> },
...> call_schema: %{
...> from: {:eoa_address, required: true},
...> to: {:eoa_address, required: true},
...> amount: {:loop, required: true}
...> },
...> timeout: 5_000
...> )
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "hx2e243ad926ac48d15156756fce28314357d49d83",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
"""
@spec transaction_call(Identity.t(), SCORE.t(), binary()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec transaction_call(
Identity.t(),
SCORE.t(),
binary(),
nil | map() | keyword()
) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec transaction_call(
Identity.t(),
SCORE.t(),
binary(),
nil | map() | keyword(),
keyword()
) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def transaction_call(identity, score, method, params \\ nil, options \\ [])
def transaction_call(%Identity{} = identity, to, method, call_params, options) do
options =
case options[:call_schema] do
nil ->
options
value ->
options
|> Keyword.delete(:call_schema)
|> Keyword.put(:schema, value)
end
with {:ok, request} <-
Request.Goloop.transaction_call(
identity,
to,
method,
call_params,
options
) do
send_transaction(request)
end
end
@doc """
Creates a new SCORE.
The `identity` should be created using a valid `private_key`, otherwise the
message cannot be sent.
Options:
- `timeout` - Time in milliseconds to wait for the transfer result.
- `params` - Extra transaction parameters for overriding the defaults.
- `content_type` - MIME type of the SCORE contents. Defaults to
`application/zip`.
- `on_install_params` - Parameters for the function `on_install/0`.
- `on_install_schema` - Schema for the parameters of the function
`on_install/0`.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Examples
- Creates a new contract:
```elixir
iex> {:ok, content} = File.read("./my-contract.javac")
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.install_score(identity, content)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Creates a new contract and waits 5 seconds for the result:
```elixir
iex> {:ok, content} = File.read("./my-contract.javac")
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.install_score(identity, content, timeout: 5_000)
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "cx0000000000000000000000000000000000000000",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
- Creates a new contract and passes paremeters to the `on_install` function:
```elixir
iex> {:ok, content} = File.read("./my-contract.javac")
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> Icon.install_score(identity, content,
...> on_install_params: %{
...> address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
...> },
...> on_install_schema: %{
...> address: {:address, required: true}
...> }
...> )
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
"""
@spec install_score(Identity.t(), BinaryData.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec install_score(Identity.t(), BinaryData.t(), keyword()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def install_score(identity, content, options \\ [])
def install_score(%Identity{} = identity, content, options) do
with {:ok, request} <-
Request.Goloop.install_score(identity, content, options) do
send_transaction(request)
end
end
@doc """
Updates a SCORE.
The `identity` should be created using a valid `private_key`, otherwise the
message cannot be sent.
Options:
- `timeout` - Time in milliseconds to wait for the transfer result.
- `params` - Extra transaction parameters for overriding the defaults.
- `content_type` - MIME type of the SCORE contents. Defaults to
`application/zip`.
- `on_update_params` - Parameters for the function `on_update/0`.
- `on_update_schema` - Schema for the parameters of the function
`on_update/0`.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Examples
- Updates a contract:
```elixir
iex> {:ok, content} = File.read("./my-contract.javac")
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.update_score(identity, score_address, content)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Updates a contract and waits 5 seconds for the result:
```elixir
iex> {:ok, content} = File.read("./my-contract.javac")
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.update_score(identity, score_address, content, timeout: 5_000)
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
- Updates a contract and passes paremeters to the `on_update` function:
```elixir
iex> {:ok, content} = File.read("./my-contract.javac")
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.update_score(identity, score_address, content,
...> on_update_params: %{
...> address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
...> },
...> on_update_schema: %{
...> address: {:address, required: true}
...> }
...> )
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
"""
@spec update_score(Identity.t(), SCORE.t(), BinaryData.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec update_score(Identity.t(), SCORE.t(), BinaryData.t(), keyword()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def update_score(identity, score_address, content, options \\ [])
def update_score(%Identity{} = identity, to, content, options) do
with {:ok, request} <-
Request.Goloop.update_score(identity, to, content, options) do
send_transaction(request)
end
end
@doc """
Deposits ICX in loop (1 ICX = 10¹⁸ loop) into a SCORE for paying user's fees
when they transact with the contract (fee sharing).
Options:
- `timeout` - Time in milliseconds to wait for the transaction result.
- `params` - Extra transaction parameters for overriding the defaults.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Examples
- Deposits `1.00` ICX in a SCORE:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.deposit_shared_fee(identity, score_address, 1_000_000_000_000_000_000)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Deposit `1.00` ICX in a SCORE and wait 5 seconds for the result:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.deposit_shared_fee(identity, score_address, 1_000_000_000_000_000_000,
...> timeout: 5_000
...> )
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
"""
@spec deposit_shared_fee(Identity.t(), SCORE.t(), Loop.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec deposit_shared_fee(Identity.t(), SCORE.t(), Loop.t(), keyword()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def deposit_shared_fee(identity, score_address, amount, options \\ [])
def deposit_shared_fee(%Identity{} = identity, to, amount, options) do
with {:ok, request} <-
Request.Goloop.deposit_shared_fee(identity, to, amount, options) do
send_transaction(request)
end
end
@doc """
Withdraws ICX from a SCORE that was destined for paying user's fees when they
transact with the contract (fee sharing).
Options:
- `timeout` - Time in milliseconds to wait for the transaction result.
- `params` - Extra transaction parameters for overriding the defaults.
While technically any parameter can be overriden with the `params` option, not
all of them make sense to do so. The following are some of the most usuful
parameters to modify via this option:
- `nonce` - An arbitrary number used to prevent transaction hash collision.
- `timestamp` - Transaction creation time. Timestamp is in microsecond.
- `stepLimit` - Maximum step allowance that can be used by the transaction.
## Examples
- Withdraws `1.00` ICX from a SCORE:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.withdraw_shared_fee(identity, score_address, 1_000_000_000_000_000_000)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Withdraws ICX using the deposit hash:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> hash = "0xc71303ef8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"
iex> Icon.withdraw_shared_fee(identity, score_address, hash)
{:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
```
- Withdraw the whole deposit from a SCORE and wait 5 seconds for the result:
```elixir
iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
iex> Icon.withdraw_shared_fee(identity, score_address, timeout: 5_000)
{
:ok,
%Icon.Schema.Types.Transaction.Result{
blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
blockHeight: 2_427_717,
cumulativeStepUsed: 0,
failure: nil,
logsBloom: <<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, 0, 0, 0, 0, ...>>,
scoreAddress: nil,
status: :success,
stepPrice: 12_500_000_000,
stepUsed: 100_000,
to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
txIndex: 1
}
}
```
"""
@spec withdraw_shared_fee(Identity.t(), SCORE.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec withdraw_shared_fee(Identity.t(), SCORE.t(), nil | Loop.t() | Hash.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
@spec withdraw_shared_fee(
Identity.t(),
SCORE.t(),
nil | Loop.t() | Hash.t(),
keyword()
) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
def withdraw_shared_fee(
identity,
score_address,
hash_or_amount \\ nil,
options \\ []
)
def withdraw_shared_fee(%Identity{} = identity, to, hash_or_amount, options) do
with {:ok, request} <-
Request.Goloop.withdraw_shared_fee(
identity,
to,
hash_or_amount,
options
) do
send_transaction(request)
end
end
#########
# Helpers
@spec load_call_response(any(), keyword()) ::
{:ok, any()} | {:error, Error.t()}
defp load_call_response(response, options)
defp load_call_response(response, options) do
response_schema = options[:response_schema] || :any
response_schema
|> Schema.generate()
|> Schema.new(response)
|> Schema.load()
|> apply_call_response(response_schema)
|> case do
{:ok, _} = ok ->
ok
{:error, _} ->
reason =
Error.new(
reason: :server_error,
message: "cannot cast call response"
)
{:error, reason}
end
end
@spec apply_call_response(Schema.state(), Schema.t()) ::
{:ok, any()}
| {:error, Error.t()}
defp apply_call_response(state, schema) do
if is_atom(schema) and function_exported?(schema, :__schema__, 0) do
Schema.apply(state, into: schema)
else
Schema.apply(state)
end
end
@spec send_transaction(Request.t()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
defp send_transaction(request)
defp send_transaction(%Request{} = request) do
with {:ok, request} <- Request.add_step_limit(request),
{:ok, request} <- Request.sign(request),
{:ok, response} <- Request.send(request) do
load_transaction_response(response)
end
end
@spec load_transaction_response(any()) ::
{:ok, Hash.t()}
| {:ok, Transaction.Result.t()}
| {:error, Error.t()}
defp load_transaction_response(data)
defp load_transaction_response("0x" <> _ = data) do
{:ok, data}
end
defp load_transaction_response(data) do
load_transaction_result(data)
end
@spec load_transaction_result(any()) ::
{:ok, Transaction.Result.t()}
| {:error, Error.t()}
defp load_transaction_result(data)
defp load_transaction_result(data) when is_map(data) do
Transaction.Result
|> Schema.generate()
|> Schema.new(data)
|> Schema.load()
|> Schema.apply(into: Transaction.Result)
end
defp load_transaction_result(_) do
reason =
Error.new(
reason: :server_error,
message: "cannot cast transaction result"
)
{:error, reason}
end
@spec load_block(any()) ::
{:ok, Block.t()}
| {:error, Error.t()}
defp load_block(data)
defp load_block(data) when is_map(data) do
Block
|> Schema.generate()
|> Schema.new(data)
|> Schema.load()
|> Schema.apply(into: Block)
end
defp load_block(_) do
reason =
Error.new(
reason: :server_error,
message: "cannot cast block"
)
{:error, reason}
end
@spec load_transaction(any()) ::
{:ok, Transaction.t()}
| {:error, Error.t()}
defp load_transaction(data)
defp load_transaction(data) when is_map(data) do
Transaction
|> Schema.generate()
|> Schema.new(data)
|> Schema.load()
|> Schema.apply(into: Transaction)
end
defp load_transaction(_) do
reason =
Error.new(
reason: :server_error,
message: "cannot cast transaction"
)
{:error, reason}
end
end