diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index d739017..405761d 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -52,8 +52,6 @@ jobs: --health-timeout 5s --health-retries 5 - env: - MIX_ENV: test steps: - name: Checkout @@ -78,6 +76,14 @@ jobs: mix ecto.drop mix ecto.create mix ecto.migrate + + - name: Create .env file + run: | + echo "PRIVATE_KEY=${{ secrets.PRIVATE_KEY }}" >> .env + echo "ADDRESS=${{ secrets.ADDRESS }}" >> .env + echo "PROVIDER_URL=${{ secrets.PROVIDER_URL }}" >> .env + echo "CONTRACT_ADDRESS=${{ secrets.CONTRACT_ADDRESS }}" >> .env + echo "CHAIN_ID=${{ secrets.CHAIN_ID }}" >> .env - name: Run tests env: MIX_ENV: test diff --git a/backend/config/runtime.exs b/backend/config/runtime.exs index b7b1d39..681230c 100644 --- a/backend/config/runtime.exs +++ b/backend/config/runtime.exs @@ -20,6 +20,15 @@ if System.get_env("PHX_SERVER") do config :peach, PeachWeb.Endpoint, server: true end +env = Dotenv.load() + +config :peach, Peach.Config, + private_key: env.values["PRIVATE_KEY"], + address: env.values["ADDRESS"], + provider_url: env.values["PROVIDER_URL"], + contract_address: env.values["CONTRACT_ADDRESS"], + chain_id: env.values["CHAIN_ID"] + if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || diff --git a/backend/lib/peach/calldata_builder.ex b/backend/lib/peach/calldata_builder.ex new file mode 100644 index 0000000..85ca458 --- /dev/null +++ b/backend/lib/peach/calldata_builder.ex @@ -0,0 +1,87 @@ +defmodule Peach.CalldataBuilder do + @moduledoc """ + Converts an Event struct into a calldata list for the smart contract. + """ + + import Bitwise + + def build_calldata(event) do + # 1. Event ID as u64 + event_id = to_u64(event.id) + + # 2. Ticket Tiers + ticket_tiers_params = build_ticket_tiers_params(event.ticket_tiers) + + # Combine all parts into the calldata list + ["0x" <> Integer.to_string(event_id, 16)] ++ + Enum.map(ticket_tiers_params, &("0x" <> Integer.to_string(&1, 16))) ++ + [event.treasury] + end + + defp to_u64(value) do + value + |> to_integer() + |> check_integer_size(64) + end + + defp build_ticket_tiers_params(ticket_tiers) do + # Number of ticket tiers + len = length(ticket_tiers) + + # Flattened list of ticket tier parameters + tiers_params = + ticket_tiers + |> Enum.flat_map(&ticket_tier_to_params/1) + + [len] ++ tiers_params + end + + defp ticket_tier_to_params(tier) do + # Convert price to u256 (two u128) + {price_low, price_high} = to_u256(tier.price) + + # Convert max_supply to u32 + max_supply = tier.max_supply |> to_integer() |> check_integer_size(32) + + [price_low, price_high, max_supply] + end + + defp to_u256(value) do + # Convert the value to a big integer + bigint = to_integer(value) + + # Split into low and high 128 bits + price_low = bigint &&& (1 <<< 128) - 1 + price_high = bigint >>> 128 + + {price_low, price_high} + end + + defp check_integer_size(value, bits) do + max_value = (1 <<< bits) - 1 + + if value < 0 or value > max_value do + raise ArgumentError, "Value #{value} does not fit in #{bits} bits" + else + value + end + end + + defp to_integer(value) do + cond do + is_integer(value) -> + value + + is_binary(value) -> + try do + String.to_integer(value) + rescue + ArgumentError -> + reraise ArgumentError, "Cannot parse integer from string: #{value}" + end + + true -> + raise ArgumentError, "Cannot convert #{inspect(value)} to integer" + end + end +end diff --git a/backend/lib/peach/config.ex b/backend/lib/peach/config.ex new file mode 100644 index 0000000..08187da --- /dev/null +++ b/backend/lib/peach/config.ex @@ -0,0 +1,27 @@ +defmodule Peach.Config do + @moduledoc """ + Provides access to application configuration. + """ + + @app :peach + + def private_key do + Application.fetch_env!(@app, __MODULE__)[:private_key] + end + + def address do + Application.fetch_env!(@app, __MODULE__)[:address] + end + + def provider_url do + Application.fetch_env!(@app, __MODULE__)[:provider_url] + end + + def contract_address do + Application.fetch_env!(@app, __MODULE__)[:contract_address] + end + + def chain_id do + Application.fetch_env!(@app, __MODULE__)[:chain_id] + end +end diff --git a/backend/lib/peach/events.ex b/backend/lib/peach/events.ex index f665645..16f3f5a 100644 --- a/backend/lib/peach/events.ex +++ b/backend/lib/peach/events.ex @@ -2,6 +2,8 @@ defmodule Peach.Events do @moduledoc """ Manages the events for the peach app """ + alias Peach.CalldataBuilder + alias Peach.Config alias Peach.Event alias Peach.Repo alias Peach.TicketTiers @@ -10,13 +12,38 @@ defmodule Peach.Events do @default_limit 50 @default_event_id 0 + @selector "0x005b3134506a8ff22ce883984545296af6e65577777882051fa04dc6ecb84e99" + @doc """ Creates an event with the given attributes. """ def create_event(event \\ %{}) do - %Event{} - |> Event.changeset(event) - |> Repo.insert() + event = + %Event{} + |> Event.changeset(event) + |> Repo.insert() + + case event do + {:ok, real_event} -> + calls = { + Config.contract_address(), + @selector, + CalldataBuilder.build_calldata(real_event) + } + + Starknet.execute_tx( + Config.provider_url(), + Config.private_key(), + Config.address(), + Config.chain_id(), + [calls] + ) + + err -> + err + end + + event end def remaining_event_tickets(event_id) do diff --git a/backend/lib/peach/ticket_tier.ex b/backend/lib/peach/ticket_tier.ex index 0a94159..5e458c9 100644 --- a/backend/lib/peach/ticket_tier.ex +++ b/backend/lib/peach/ticket_tier.ex @@ -5,11 +5,12 @@ defmodule Peach.TicketTier do use Ecto.Schema import Ecto.Changeset - @derive {Jason.Encoder, only: [:id, :name, :description, :max_supply]} + @derive {Jason.Encoder, only: [:id, :name, :description, :max_supply, :price]} schema "ticket_tiers" do field :name, :string field :description, :string field :max_supply, :integer + field :price, :integer belongs_to :event, Peach.Event @@ -19,7 +20,7 @@ defmodule Peach.TicketTier do @doc false def changeset(ticket_tier, attrs) do ticket_tier - |> cast(attrs, [:name, :description, :max_supply]) - |> validate_required([:name, :description, :max_supply]) + |> cast(attrs, [:name, :description, :max_supply, :price]) + |> validate_required([:name, :description, :max_supply, :price]) end end diff --git a/backend/lib/peach/ticket_tiers.ex b/backend/lib/peach/ticket_tiers.ex index 1700696..2aa777d 100644 --- a/backend/lib/peach/ticket_tiers.ex +++ b/backend/lib/peach/ticket_tiers.ex @@ -28,6 +28,7 @@ defmodule Peach.TicketTiers do id: ticket_tier.id, name: ticket_tier.name, description: ticket_tier.description, + price: ticket_tier.price, remaining: ticket_tier.max_supply - sold_tickets, max_supply: ticket_tier.max_supply }} diff --git a/backend/lib/peach_web/controllers/event_controller.ex b/backend/lib/peach_web/controllers/event_controller.ex index 120d7d1..425de08 100644 --- a/backend/lib/peach_web/controllers/event_controller.ex +++ b/backend/lib/peach_web/controllers/event_controller.ex @@ -62,11 +62,11 @@ defmodule PeachWeb.EventController do conn |> put_status(:not_found) |> json(%{errors: "Event not found"}) - ticket_tier -> + + ticket_tier -> conn |> put_status(:ok) |> json(%{tickets: ticket_tier}) - end end end diff --git a/backend/lib/peach_web/controllers/ticket_controller.ex b/backend/lib/peach_web/controllers/ticket_controller.ex index 2e31b29..479ef35 100644 --- a/backend/lib/peach_web/controllers/ticket_controller.ex +++ b/backend/lib/peach_web/controllers/ticket_controller.ex @@ -19,6 +19,7 @@ defmodule PeachWeb.TicketController do %{ "tier_id" => tier.id, "name" => tier.name, + "price" => tier.price, "description" => tier.description, "ticket_ids" => Enum.map(tickets, & &1.id) |> Enum.sort() } diff --git a/backend/lib/starknet.ex b/backend/lib/starknet.ex index fde5dd1..e5813aa 100644 --- a/backend/lib/starknet.ex +++ b/backend/lib/starknet.ex @@ -1,4 +1,7 @@ defmodule Starknet do + @moduledoc """ + Binding to call the starknet rust sdk to execute a transaction + """ use Rustler, otp_app: :peach, crate: "starknet" # Fallback function in case the NIF is not loaded diff --git a/backend/priv/repo/migrations/20241009140616_create_ticket_tiers.exs b/backend/priv/repo/migrations/20241009140616_create_ticket_tiers.exs index 9984f2f..8e90ccb 100644 --- a/backend/priv/repo/migrations/20241009140616_create_ticket_tiers.exs +++ b/backend/priv/repo/migrations/20241009140616_create_ticket_tiers.exs @@ -6,6 +6,7 @@ defmodule Peach.Repo.Migrations.CreateTicketTiers do add :name, :string add :description, :string add :max_supply, :integer + add :price, :integer add :event_id, references(:events, on_delete: :nothing) timestamps(type: :utc_datetime) diff --git a/backend/test/native/starknet_test.exs b/backend/test/native/starknet_test.exs index 5c40dc3..ce177f5 100644 --- a/backend/test/native/starknet_test.exs +++ b/backend/test/native/starknet_test.exs @@ -4,12 +4,6 @@ defmodule StarknetTest do @moduletag :integration - @private_key System.get_env("PRIVATE_KEY") || - raise("PRIVATE_KEY environment variable is not set") - @address System.get_env("ADDRESS") || raise("ADDRESS environment variable is not set") - @provider_url System.get_env("PROVIDER_URL") || - raise("PROVIDER_URL environment variable is not set") - @chain_id "SN_SEPOLIA" setup_all do # Ensure the NIF is loaded Application.ensure_all_started(:starknet) @@ -21,11 +15,18 @@ defmodule StarknetTest do { "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "0x0083afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", - [@address, "0x01", "0x0"] + [Peach.Config.address(), "0x01", "0x0"] } ] - tx_hash = Starknet.execute_tx(@provider_url, @private_key, @address, @chain_id, calls) + tx_hash = + Starknet.execute_tx( + Peach.Config.provider_url(), + Peach.Config.private_key(), + Peach.Config.address(), + Peach.Config.chain_id(), + calls + ) assert String.starts_with?(tx_hash, "0x") end @@ -34,7 +35,14 @@ defmodule StarknetTest do provider_url = "invalid_url" calls = [] - result = Starknet.execute_tx(provider_url, @private_key, @address, @chain_id, calls) + result = + Starknet.execute_tx( + provider_url, + Peach.Config.private_key(), + Peach.Config.address(), + Peach.Config.chain_id(), + calls + ) assert {:error, :invalid_provider_url} = result end @@ -43,7 +51,14 @@ defmodule StarknetTest do private_key = "invalid_private_key" calls = [] - result = Starknet.execute_tx(@provider_url, private_key, @address, @chain_id, calls) + result = + Starknet.execute_tx( + Peach.Config.provider_url(), + private_key, + Peach.Config.address(), + Peach.Config.chain_id(), + calls + ) assert {:error, :invalid_pk} = result end @@ -52,7 +67,14 @@ defmodule StarknetTest do address = "invalid_address" calls = [] - result = Starknet.execute_tx(@provider_url, @private_key, address, @chain_id, calls) + result = + Starknet.execute_tx( + Peach.Config.provider_url(), + Peach.Config.private_key(), + address, + Peach.Config.chain_id(), + calls + ) assert {:error, :invalid_address} = result end @@ -66,7 +88,14 @@ defmodule StarknetTest do } ] - result = Starknet.execute_tx(@provider_url, @private_key, @address, @chain_id, calls) + result = + Starknet.execute_tx( + Peach.Config.provider_url(), + Peach.Config.private_key(), + Peach.Config.address(), + Peach.Config.chain_id(), + calls + ) assert {:error, :invalid_to_address} = result end diff --git a/backend/test/peach_web/controllers/create_event_test.exs b/backend/test/peach_web/controllers/create_event_test.exs index 91cd8fe..e4f1017 100644 --- a/backend/test/peach_web/controllers/create_event_test.exs +++ b/backend/test/peach_web/controllers/create_event_test.exs @@ -19,11 +19,13 @@ defmodule PeachWeb.EventCreateControllerTest do %{ "name" => "General Admission", "description" => "Access to all sessions", + "price" => 5, "max_supply" => 100 }, %{ "name" => "VIP", "description" => "Access to VIP sessions and perks", + "price" => 10, "max_supply" => 20 } ] diff --git a/backend/test/peach_web/controllers/remaining_tickets_events_test.exs b/backend/test/peach_web/controllers/remaining_tickets_events_test.exs index f5b7562..22f25b7 100644 --- a/backend/test/peach_web/controllers/remaining_tickets_events_test.exs +++ b/backend/test/peach_web/controllers/remaining_tickets_events_test.exs @@ -14,6 +14,7 @@ defmodule PeachWeb.EventControllerTest do Repo.insert!(%TicketTier{ name: "VIP", description: "Access to VIP areas", + price: 10, max_supply: 50, event_id: event.id }) @@ -22,6 +23,7 @@ defmodule PeachWeb.EventControllerTest do Repo.insert!(%TicketTier{ name: "Standard", description: "General admission", + price: 5, max_supply: 200, event_id: event.id }) @@ -50,6 +52,7 @@ defmodule PeachWeb.EventControllerTest do "id" => vip_tier.id, "name" => vip_tier.name, "description" => vip_tier.description, + "price" => 10, "max_supply" => vip_tier.max_supply, "remaining" => 48 }, @@ -57,6 +60,7 @@ defmodule PeachWeb.EventControllerTest do "id" => standard_tier.id, "name" => standard_tier.name, "description" => standard_tier.description, + "price" => 5, "max_supply" => standard_tier.max_supply, "remaining" => standard_tier.max_supply } diff --git a/backend/test/peach_web/controllers/ticket_controller_test.exs b/backend/test/peach_web/controllers/ticket_controller_test.exs index 4e193f8..f405236 100644 --- a/backend/test/peach_web/controllers/ticket_controller_test.exs +++ b/backend/test/peach_web/controllers/ticket_controller_test.exs @@ -21,6 +21,7 @@ defmodule PeachWeb.TicketControllerTest do name: "VIP", description: "Access to VIP sessions", max_supply: 100, + price: 10, event_id: event.id }) @@ -29,6 +30,7 @@ defmodule PeachWeb.TicketControllerTest do name: "Standard", description: "General admission", max_supply: 200, + price: 5, event_id: event.id }) @@ -84,12 +86,14 @@ defmodule PeachWeb.TicketControllerTest do "tier_id" => vip_ticket.ticket_tier_id, "name" => "VIP", "description" => "Access to VIP sessions", + "price" => 10, "ticket_ids" => [1, 2] }, %{ "tier_id" => standard_ticket.ticket_tier_id, "name" => "Standard", "description" => "General admission", + "price" => 5, "ticket_ids" => [3] } ] diff --git a/backend/test/peach_web/controllers/ticket_tier_controller_test.exs b/backend/test/peach_web/controllers/ticket_tier_controller_test.exs index 8f15604..1e132dd 100644 --- a/backend/test/peach_web/controllers/ticket_tier_controller_test.exs +++ b/backend/test/peach_web/controllers/ticket_tier_controller_test.exs @@ -16,6 +16,7 @@ defmodule PeachWeb.TicketTierControllerTest do name: "VIP", description: "Access to VIP areas", max_supply: 50, + price: 10, event_id: event.id }) @@ -24,6 +25,7 @@ defmodule PeachWeb.TicketTierControllerTest do name: "Standard", description: "General admission", max_supply: 200, + price: 5, event_id: event.id }) @@ -50,12 +52,14 @@ defmodule PeachWeb.TicketTierControllerTest do "id" => vip_tier.id, "name" => vip_tier.name, "description" => vip_tier.description, + "price" => vip_tier.price, "max_supply" => vip_tier.max_supply }, %{ "id" => standard_tier.id, "name" => standard_tier.name, "description" => standard_tier.description, + "price" => standard_tier.price, "max_supply" => standard_tier.max_supply } ] @@ -69,9 +73,6 @@ defmodule PeachWeb.TicketTierControllerTest do # Send the GET request with an invalid event_id conn = get(conn, "/api/events/9999/ticket_tiers") - # Expected error response - expected_error = %{"errors" => "Event not found"} - # Assert the response status and error message assert json_response(conn, 200) == %{"ticket_tiers" => []} end @@ -90,6 +91,7 @@ defmodule PeachWeb.TicketTierControllerTest do "name" => ticket_tier.name, "description" => ticket_tier.description, "max_supply" => ticket_tier.max_supply, + "price" => ticket_tier.price, # Expected remaining tickets (50 - 2 tickets sold) "remaining" => 48 }