defmodule CodesOnChain.Contracts.Bodhi do @moduledoc """ the interact with bodhi on op chain. > https://mainnet.optimism.io > 10 > https://explorer.optimism.io """ alias Ethereumex.HttpClient alias Components.Transaction require Logger @endpoint "https://mainnet.optimism.io" @chain_id 10 @contract_addr "0x2AD82A4E39Bac43A54DdfE6f94980AAf0D1409eF" @arweave_node "https://arweave.net" @func %{ asset_index: "assetIndex()", assets: "assets(uint256)", balance_of: "balanceOf(address, uint256)", buy: "buy(uint256, uint256)", get_buy_price_after_fee: "getBuyPriceAfterFee(uint256, uint256)", create: "create(string)" } @contract_addr_space_factory "0xA14D19387C83b56343fC2E7a8707986aF6a74d08" @func_space_factory %{ space_index: "spaceIndex()", spaces: "spaces(uint256)" } # An example space: 0xBEAFc083600efC2376648bFF353Ce8A3EcaA1463 @func_space %{ asset_to_creator: "assetToCreator(uint256)", create: "create(string, uint256)" } @gas_limit 1_000_000 def get_module_doc, do: @moduledoc # +--------------+ # | Space Reader | # +--------------+ def get_true_creator(contract_addr, asset_id) do data = TypeTranslator.get_data(@func_space.asset_to_creator, [asset_id]) {:ok, raw} = HttpClient.eth_call( %{ data: data, to: contract_addr }, "latest", [url: @endpoint, request_timeout: 1000] ) [addr] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([:address]) "0x" <> Base.encode16(addr, case: :lower) end # +--------------+ # | Space Write | # +--------------+ def create_and_buy_space(priv, contract_addr, ar_tx_id, amount) do asset_id = get_asset_index() # amount = 0.01 # batch send create and buy. # 0. get nonce. # gen addr from priv acct = priv |> TypeTranslator.addr_to_bin() |> EthWallet.generate_keys() nonce = Transaction.get_nonce(acct.addr, [url: @endpoint, request_timeout: 1000]) # 1. build create tx. # 2. build buy tx. data_create = ABI.encode(@func_space.create, [ar_tx_id, 0]) # get gas {:ok, gas_price} = HttpClient.eth_gas_price([url: @endpoint, request_timeout: 1000]) gas_limit = @gas_limit tx_create = %Transaction{ from: acct.addr, to: contract_addr, gas_limit: gas_limit, gas_price: TypeTranslator.hex_to_int(gas_price), value: 0, data: data_create } amount_raw = "#{amount}" |> Decimal.new() |> Decimal.mult(1_000_000_000_000_000_000) |> Decimal.to_integer() # get data data = ABI.encode(@func.buy, [asset_id, amount_raw]) # get the buy price. # price_raw = 21000000000000 price_raw = get_buy_price_after_fee(asset_id, amount) tx = %Transaction{ from: acct.addr, to: @contract_addr, gas_limit: gas_limit, gas_price: TypeTranslator.hex_to_int(gas_price), value: price_raw, data: data } # 3. send & send. tx_result = Transaction.send( @chain_id, acct.priv, tx_create, nonce, [url: @endpoint, request_timeout: 500]) tx_result_2 = Transaction.send( @chain_id, acct.priv, tx, nonce + 1 , [url: @endpoint, request_timeout: 500]) %{tx_result_1: tx_result, tx_result_2: tx_result_2} end # +--------------+ # | Asset Reader | # +--------------+ def get_buy_price_after_fee(asset_id, amount) do amount_raw = "#{amount}" |> Decimal.new() |> Decimal.mult(1_000_000_000_000_000_000) |> Decimal.to_integer() data = TypeTranslator.get_data(@func.get_buy_price_after_fee, [asset_id, amount_raw]) {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr }, "latest", [url: @endpoint, request_timeout: 1000] ) # decode [price_raw] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([{:uint, 256}]) price_raw + 1_500_000_000_000_000 end def balance_of(addr, asset_id) do # addr_bin addr_bin = TypeTranslator.addr_to_bin(addr) # get data data = TypeTranslator.get_data(@func.balance_of, [addr_bin, asset_id]) # get raw response {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr }, "latest", [url: @endpoint, request_timeout: 1000] ) # decode [balance] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([{:uint, 256}]) balance |> Decimal.div(1_000_000_000_000_000_000) |> Decimal.to_float() end # +------------------------+ # | RAW Data Fetcher Space | # +------------------------+ def get_space_index() do data = TypeTranslator.get_data(@func_space_factory.space_index, []) {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr_space_factory }, "latest", [url: @endpoint, request_timeout: 1000] ) [index] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([{:uint, 256}]) index end def get_spaces(begin_space_id, end_space_id) do Enum.map(begin_space_id..end_space_id, fn space_id -> get_space(space_id) end) end # TODO: check events to get all the assets under this space. # I think that it could be use api of opscan here. def get_logs(contract_addr, begin_block) do end def get_space(space_id) do # get data data = TypeTranslator.get_data(@func_space_factory.spaces, [space_id]) # get raw response {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr_space_factory }, "latest", [url: @endpoint, request_timeout: 1000] ) # decode [addr] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([:address]) "0x" <> Base.encode16(addr, case: :lower) end # +------------------+ # | RAW Data Fetcher | # +------------------+ def get_assets(begin_asset_id, end_asset_id) do Enum.map(begin_asset_id..end_asset_id, fn asset_id -> get_asset(asset_id) end) end def get_asset_index() do data = TypeTranslator.get_data(@func.asset_index, []) {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr }, "latest", [url: @endpoint, request_timeout: 1000] ) [index] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([{:uint, 256}]) index end def get_asset(asset_id) do # get data data = TypeTranslator.get_data(@func.assets, [asset_id]) # get raw response {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr }, "latest", [url: @endpoint, request_timeout: 1000] ) # decode [asset_id, ar_resource, addr] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([{:uint, 256}, :string, :address]) addr = "0x" <> Base.encode16(addr, case: :lower) [asset_id, ar_resource, addr] end # +----------------+ # | Write Tx Funcs | # +----------------+ def create_and_buy(priv, ar_tx_id, amount) do asset_id = get_asset_index() # amount = 0.01 # batch send create and buy. # 0. get nonce. # gen addr from priv acct = priv |> TypeTranslator.addr_to_bin() |> EthWallet.generate_keys() nonce = Transaction.get_nonce(acct.addr, [url: @endpoint, request_timeout: 1000]) # 1. build create tx. # 2. build buy tx. data_create = ABI.encode(@func.create, [ar_tx_id]) # get gas {:ok, gas_price} = HttpClient.eth_gas_price([url: @endpoint, request_timeout: 1000]) gas_limit = @gas_limit tx_create = %Transaction{ from: acct.addr, to: @contract_addr, gas_limit: gas_limit, gas_price: TypeTranslator.hex_to_int(gas_price), value: 0, data: data_create } amount_raw = "#{amount}" |> Decimal.new() |> Decimal.mult(1_000_000_000_000_000_000) |> Decimal.to_integer() # get data data = ABI.encode(@func.buy, [asset_id, amount_raw]) # get the buy price. # price_raw = 21000000000000 price_raw = get_buy_price_after_fee(asset_id, amount) tx = %Transaction{ from: acct.addr, to: @contract_addr, gas_limit: gas_limit, gas_price: TypeTranslator.hex_to_int(gas_price), value: price_raw, data: data } # 3. send & send. tx_result = Transaction.send( @chain_id, acct.priv, tx_create, nonce, [url: @endpoint, request_timeout: 500]) Logger.info(inspect(tx_result)) tx_result_2 = Transaction.send( @chain_id, acct.priv, tx, nonce + 1 , [url: @endpoint, request_timeout: 500]) Logger.info(inspect(tx_result_2)) end def buy(priv, asset_id, amount) do # gen amount_raw amount_raw = "#{amount}" |> Decimal.new() |> Decimal.mult(1_000_000_000_000_000_000) |> Decimal.to_integer() # gen addr from priv acct = priv |> TypeTranslator.addr_to_bin() |> EthWallet.generate_keys() # get the buy price. # price_raw = 21000000000000 price_raw = get_buy_price_after_fee(asset_id, amount) # get gas gas_limit = @gas_limit {:ok, gas_price} = HttpClient.eth_gas_price([url: @endpoint, request_timeout: 1000]) # get data data = ABI.encode(@func.buy, [asset_id, amount_raw]) # buy the asset. tx = %Transaction{ from: acct.addr, to: @contract_addr, gas_limit: gas_limit, gas_price: TypeTranslator.hex_to_int(gas_price), value: price_raw, data: data } # Transaction.send( # @chain_id, # acct.priv, # tx, # [url: @endpoint, request_timeout: 1000]) tx_result = Transaction.send( @chain_id, acct.priv, tx, [url: @endpoint, request_timeout: 1000]) inspect(tx_result) end # +----------------------+ # | Text Data Translator | # +----------------------+ @doc """ params: { "type": "INSERT", "table": "raw_data", "record": { "id": 13, "ar_tx_id": "5HpDDzIe40n3c_7CH-3XECQxTf14VeUfAgfxkc3BZDo", "content": "fdsfsdsfdsfd", "created_at": "2023-12-16T22:22:08.542514+00:00" }, "schema": "public", "old_record": null } """ def supabase_raw_to_text_data(payload) do %{ id_on_chain: id_on_chain, ar_tx_id: ar_tx_id, creator: creator } = payload.record content = get_content(ar_tx_id) tags = case get_tags(ar_tx_id) do {:ok, tags} -> tags _ -> %{} end result = if is_map(content) do cond do content.type in ["text/markdown; charset=utf-8", "text/plain; charset=utf-8"] -> %{ id_on_chain: id_on_chain, content: content.content, creator: creator, tags: tags } true -> "pass" end else "pass" end result end def supabase_raw_to_img_data(payload) do %{ id_on_chain: id_on_chain, ar_tx_id: ar_tx_id, creator: creator } = payload.record content = get_content(ar_tx_id) result = if is_map(content) do cond do String.contains?(content.type, "image") -> %{ id_on_chain: id_on_chain, content: content.content, creator: creator } true -> "pass" end else "pass" end result end def supabase_get_assets(from, to) do Enum.map(from..to, fn asset -> supabase_get_asset(asset) end) end def supabase_get_asset(asset_id) do # get data data = TypeTranslator.get_data(@func.assets, [asset_id]) # get raw response {:ok, raw} = HttpClient.eth_call( %{ data: data, to: @contract_addr }, "latest", [url: @endpoint, request_timeout: 1000] ) # decode [asset_id, ar_resource, addr] = raw |> Binary.drop(2) |> Base.decode16!(case: :lower) |> ABI.TypeDecoder.decode_raw([{:uint, 256}, :string, :address]) addr = "0x" <> Base.encode16(addr, case: :lower) %{ id_on_chain: asset_id, ar_tx_id: ar_resource, creator: addr } end def supabase_text_data_to_vector_data(payload) do %{ id_on_chain: id_on_chain, content: content, creator: creator } = payload.record metadata = %{id: id_on_chain, type: "text/markdown; charset=utf-8", creator: creator} paragraphs = String.split(content, "\n") Enum.map(paragraphs, fn paragraph -> %{data: paragraph, metadata: metadata} end) |> List.flatten() |> Enum.reject(fn %{data: data, metadata: _metadata} -> data == "\r" end) end def handle_audio(content) do if String.contains?(content, " {:ok, tags} {:error, "404"} -> {:error, "404"} error -> {:error, "Unexpected error: #{inspect(error)}"} end rescue error -> {:error, inspect(error)} end end def get_content(ar_tx_id) do try do {:ok, content} = ArweaveSdkEx.get_content_in_tx(@arweave_node, ar_tx_id) content rescue error -> inspect(error) end end def to_text_database(asset_lists) do asset_lists |> Enum.map(fn [id_on_chain, ar_tx_id, creator] -> content = get_content(ar_tx_id) Logger.info("-- get_content_from_arweave --") [id_on_chain, content, creator] end) |> Enum.filter(fn [_id_on_chain, content, _creator] -> is_map(content) end) |> Enum.filter(fn [_id_on_chain, content, _creator] -> content.type == "text/markdown; charset=utf-8" end) |> Enum.map(fn [id_on_chain, content, creator] -> [id_on_chain, content.content, creator] end) end # +--------------------------+ # | VectorDB Data Translator | # +--------------------------+ def to_vector_db(asset_lists) do asset_lists |> Enum.map(fn [id_on_chain, content, creator] -> metadata = %{id: id_on_chain, type: "text/markdown; charset=utf-8", creator: creator} paragraphs = String.split(content, "\n") Enum.map(paragraphs, fn paragraph -> %{data: paragraph, metadata: metadata} end) end) |> List.flatten() |> Enum.reject(fn %{data: data, metadata: _metadata} -> data == "\r" end) end end