defmodule Rujira.Contracts do
@moduledoc """
Convenience methods for querying CosmWasm smart contracts.
"""
alias Cosmos.Base.Query.V1beta1.PageRequest
alias Cosmwasm.Wasm.V1.CodeInfoResponse
alias Cosmwasm.Wasm.V1.ContractInfo
alias Cosmwasm.Wasm.V1.Model
alias Cosmwasm.Wasm.V1.Query.Stub
alias Cosmwasm.Wasm.V1.QueryAllContractStateRequest
alias Cosmwasm.Wasm.V1.QueryBuildAddressRequest
alias Cosmwasm.Wasm.V1.QueryCodeRequest
alias Cosmwasm.Wasm.V1.QueryCodeResponse
alias Cosmwasm.Wasm.V1.QueryCodesRequest
alias Cosmwasm.Wasm.V1.QueryContractInfoRequest
alias Cosmwasm.Wasm.V1.QueryContractsByCodeRequest
alias Cosmwasm.Wasm.V1.QueryRawContractStateRequest
alias Cosmwasm.Wasm.V1.QuerySmartContractStateRequest
alias Rujira.Logger
use Memoize
defstruct id: nil, address: nil, info: nil
@type t :: %__MODULE__{id: String.t(), address: String.t(), info: ContractInfo.t() | nil}
@spec from_id(String.t()) :: {:ok, t()}
def from_id(id) do
{:ok, %__MODULE__{id: id, address: id}}
end
@spec code_info(non_neg_integer()) ::
{:ok, CodeInfoResponse.t()} | {:error, GRPC.RPCError.t()}
defmemo code_info(code_id) do
with {:ok, %{code_info: code_info}} <-
Rujira.Node.query(&Stub.code/2, %QueryCodeRequest{code_id: code_id}) do
{:ok, code_info}
end
end
@spec version(String.t()) ::
{:ok, %{contract: String.t(), version: String.t()} | nil} | {:error, term()}
defmemo version(address) do
case query_state_raw(address, :erlang.iolist_to_binary("contract_info")) do
{:ok, %{"contract" => contract, "version" => version}} ->
{:ok, %{contract: contract, version: version}}
{:error, %{message: "codespace wasm code 22: no such contract:" <> _}} ->
{:ok, nil}
other ->
other
end
end
@spec build_address(binary(), String.t(), non_neg_integer() | String.t()) ::
{:ok, String.t()} | {:error, term()}
defmemo build_address(salt, creator, id) when is_integer(id) do
with {:ok, %{data_hash: data_hash}} <- code_info(id) do
build_address(salt, creator, Base.encode16(data_hash))
end
end
defmemo build_address(salt, creator, hash) do
with {:ok, %{address: address}} <-
Rujira.Node.query(
&Stub.build_address/2,
%QueryBuildAddressRequest{
code_hash: hash,
creator_address: creator,
salt: salt
}
) do
{:ok, address}
end
end
@spec build_address!(binary(), String.t(), non_neg_integer() | String.t()) :: String.t()
defmemo build_address!(salt, deployer, code_id) do
{:ok, address} = build_address(salt, deployer, code_id)
address
end
@spec info(String.t()) ::
{:ok, ContractInfo.t()} | {:error, GRPC.RPCError.t()}
defmemo info(address) do
with {:ok, %{contract_info: contract_info}} <-
Rujira.Node.query(
&Stub.contract_info/2,
%QueryContractInfoRequest{address: address}
) do
{:ok, contract_info}
end
end
@spec codes() :: {:ok, list(CodeInfoResponse.t())} | {:error, GRPC.RPCError.t()}
defmemo codes() do
codes_page()
end
defp codes_page(key \\ nil)
defp codes_page(nil) do
with {:ok, %{code_infos: code_infos, pagination: %{next_key: next_key}}} <-
Rujira.Node.query(&Stub.codes/2, %QueryCodesRequest{}),
{:ok, next} <- codes_page(next_key) do
{:ok, Enum.concat(code_infos, next)}
end
end
defp codes_page(""), do: {:ok, []}
defp codes_page(key) do
with {:ok, %{code_infos: code_infos, pagination: %{next_key: next_key}}} <-
Rujira.Node.query(
&Stub.codes/2,
%QueryCodesRequest{pagination: %PageRequest{key: key}}
),
{:ok, next} <- codes_page(next_key) do
{:ok, Enum.concat(code_infos, next)}
end
end
@spec by_code(integer()) ::
{:ok, list(t())} | {:error, GRPC.RPCError.t()}
defmemo by_code(code_id) do
with {:ok, contracts} <- by_code_page(code_id) do
{:ok, Enum.map(contracts, &%__MODULE__{id: &1, address: &1})}
end
end
defp by_code_page(code_id, key \\ nil)
defp by_code_page(code_id, nil) do
with {:ok, %{contracts: contracts, pagination: %{next_key: next_key}}} <-
Rujira.Node.query(
&Stub.contracts_by_code/2,
%QueryContractsByCodeRequest{code_id: code_id}
),
{:ok, next} <- by_code_page(code_id, next_key) do
{:ok, Enum.concat(contracts, next)}
end
end
defp by_code_page(_code_id, ""), do: {:ok, []}
defp by_code_page(code_id, key) do
with {:ok, %{contracts: contracts, pagination: %{next_key: next_key}}} <-
Rujira.Node.query(
&Stub.contracts_by_code/2,
%QueryContractsByCodeRequest{
code_id: code_id,
pagination: %PageRequest{key: key}
}
),
{:ok, next} <- by_code_page(code_id, next_key) do
{:ok, Enum.concat(contracts, next)}
end
end
@spec code(integer()) :: {:ok, QueryCodeResponse} | {:error, GRPC.RPCError.t()}
defmemo code(id) do
with {:ok, %{code_info: code_info}} <-
Rujira.Node.query(&Stub.code/2, %QueryCodeRequest{code_id: id}) do
{:ok, code_info}
end
end
@spec by_codes(list(integer())) ::
{:ok, list(t())} | {:error, GRPC.RPCError.t()}
def by_codes(code_ids) do
Enum.reduce(code_ids, {:ok, []}, fn
el, {:ok, agg} ->
case by_code(el) do
{:ok, contracts} -> {:ok, agg ++ contracts}
err -> err
end
_, err ->
err
end)
end
@spec get({module(), String.t() | __MODULE__.t()} | struct()) ::
{:ok, struct()} | {:error, any()}
defmemo(get({module, %__MODULE__{address: address}}), do: get({module, address}))
defmemo get({module, address}) do
case query_state_smart(address, %{config: %{}}) do
{:ok, config} -> construct(module, address, config)
err -> err
end
end
# TODO: remove the `from_config/2` fallback once rujira-api has migrated every
# protocol module to expose `new/1`. Tracks the incremental adoption of
# rujira_ex by rujira-api — see CONTRIBUTING.md.
defp construct(module, address, config) do
Code.ensure_loaded(module)
cond do
function_exported?(module, :new, 1) ->
config |> Map.put("address", address) |> module.new()
function_exported?(module, :from_config, 2) ->
module.from_config(address, config)
true ->
{:error, {:no_constructor, module}}
end
end
@spec list(module(), list(integer())) ::
{:ok, list(struct())} | {:error, GRPC.RPCError.t()}
defmemo list(module, code_ids) when is_list(code_ids) do
with {:ok, contracts} <- by_codes(code_ids),
{:ok, struct} <-
contracts
|> Rujira.Enum.reduce_async_while_ok(&get({module, &1}), timeout: 30_000) do
{:ok, struct}
end
end
@spec query_state_raw(String.t(), binary()) ::
{:ok, term()} | {:error, :not_found} | {:error, GRPC.RPCError.t()}
def query_state_raw(address, query) do
case Rujira.Node.query(
&Stub.raw_contract_state/2,
%QueryRawContractStateRequest{
address: address,
query_data: query
}
) do
{:ok, %{data: ""}} -> {:error, :not_found}
{:ok, %{data: data}} -> JSON.decode(data)
other -> other
end
end
@spec query_state_smart(String.t(), map()) ::
{:ok, map()} | {:error, GRPC.RPCError.t()}
def query_state_smart(address, query) do
with {:ok, %{data: data}} <-
Rujira.Node.query(&Stub.smart_contract_state/2, %QuerySmartContractStateRequest{
address: address,
query_data: JSON.encode!(query)
}) do
JSON.decode(data)
end
end
@spec query_state_smart(String.t(), map(), keyword()) ::
{:ok, map()} | {:error, GRPC.RPCError.t()}
def query_state_smart(address, query, opts) do
with {:ok, %{data: data}} <-
Rujira.Node.query(
&Stub.smart_contract_state/3,
%QuerySmartContractStateRequest{
address: address,
query_data: JSON.encode!(query)
},
opts
) do
JSON.decode(data)
end
end
@doc """
Paginates through a smart contract query result set.
"""
@spec paginate(
{:ok, map()} | {:error, any()},
String.t(),
pos_integer(),
(list() -> {:ok, list()} | {:error, any()})
) :: {:ok, list()} | {:error, any()}
def paginate(result, key, limit, next_fn)
def paginate({:ok, %{} = res}, key, limit, next_fn) do
items = Map.get(res, key, [])
if length(items) == limit do
with {:ok, next} <- next_fn.(items), do: {:ok, items ++ next}
else
{:ok, items}
end
end
def paginate(err, _, _, _), do: err
@doc "Queries the full, raw contract state at an address"
@spec query_state_all(String.t()) ::
{:ok, map()} | {:error, GRPC.RPCError.t()}
defmemo query_state_all(address) do
query_state_all_page(address, nil)
end
defp query_state_all_page(address, page) do
with {:ok, %{models: models, pagination: %{next_key: next_key}}} when next_key != "" <-
Rujira.Node.query(
&Stub.all_contract_state/2,
%QueryAllContractStateRequest{address: address, pagination: page}
),
{:ok, next} <-
query_state_all_page(address, %PageRequest{key: next_key}) do
{:ok, decode_models(models, next)}
else
{:ok, %{models: models, pagination: %{next_key: nil}}} ->
{:ok, decode_models(models)}
{:ok, %{models: models, pagination: %{next_key: ""}}} ->
{:ok, decode_models(models)}
err ->
err
end
end
@doc "Streams the current contract state"
@spec stream_state_all(String.t()) :: Enumerable.t()
def stream_state_all(address) do
Stream.resource(
fn ->
Rujira.Node.query(
&Stub.all_contract_state/2,
%QueryAllContractStateRequest{address: address}
)
end,
fn
{:ok,
%{
models: [%{value: value}],
pagination: %{next_key: next_key}
}}
when next_key != "" ->
next =
Rujira.Node.query(
&Stub.all_contract_state/2,
%QueryAllContractStateRequest{
address: address,
pagination: %PageRequest{key: next_key}
}
)
{[JSON.decode!(value)], next}
{:ok, %{models: [%{value: value} | xs]} = agg} ->
{[JSON.decode!(value)], {:ok, %{agg | models: xs}}}
{:ok, %{models: [], pagination: %{next_key: ""}}} = acc ->
{:halt, acc}
end,
fn _ -> nil end
)
end
@spec query_state_smart_with_retry(String.t(), map()) ::
{:ok, map()} | {:error, term()}
def query_state_smart_with_retry(address, query) do
case query_state_smart(address, query) |> log_retry(address, query) do
{:error, %GRPC.RPCError{status: 2, message: msg}}
when msg in [
"Invalid layer 1 string : query wasm contract failed",
"codespace wasm code 29: wasmvm error: Error calling the VM: Error executing Wasm: Wasmer runtime error: RuntimeError: Error calling into the VM's backend: Panic in FFI call",
"Generic error: Parsing u128: cannot parse integer from empty string: query wasm contract failed",
"failed to decode Protobuf message: invalid tag value: 0: query wasm contract failed",
"Generic error: Parsing u128: invalid digit found in string: query wasm contract failed",
"Generic error: Error parsing whole: query wasm contract failed"
] ->
query_state_smart(address, query)
other ->
other
end
end
# --- Private ---
defp decode_models(models, init \\ %{}) do
Enum.reduce(models, init, fn %Model{} = model, agg ->
Map.put(agg, model.key, JSON.decode!(model.value))
end)
end
defp log_retry({:error, %GRPC.RPCError{status: status, message: message}} = err, address, query) do
Logger.error(__MODULE__, "GRPC Retry: #{address} #{inspect(query)} #{status} #{message}")
err
end
defp log_retry(other, _, _), do: other
end