Skip to content

Commit

Permalink
feat: connect gitcoin score strategy to action execution flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ribeirojose committed Oct 7, 2024
1 parent eb71a4e commit 87af4af
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ class NotStartedError < StandardError; end

class AlreadyCompletedError < StandardError; end

class ExecutionExpiredError < StandardError; end

class InvalidNonceError < StandardError; end

EXPIRATION_TIME_IN_SECONDS = 30 * 60

def initialize(id)
@id = id
@action_id = nil
Expand All @@ -31,7 +27,6 @@ def start(action_id, action_type, user_id, start_data)
nonce = SecureRandom.hex(16)
strategy = ActionTracking::ActionStrategyFactory.for(action_type)
data = strategy.start_execution(start_data)

apply ActionExecutionStarted.new(data: {
execution_id: @id,
action_id: action_id,
Expand All @@ -47,32 +42,18 @@ def complete(nonce, completion_data)
raise InvalidNonceError unless valid_nonce?(nonce)
raise NotStartedError if @state == "not_started"
raise AlreadyCompletedError if @state == "completed"
raise ExecutionExpiredError if expired?

strategy = ActionTracking::ActionStrategyFactory.for(@action_type)
data = strategy.complete_execution(completion_data.merge(@data))
data = strategy.complete_execution(completion_data)

apply ActionExecutionCompleted.new(data: {
execution_id: @id,
completion_data: completion_data.merge(data || {})
completion_data: data
})
end

def expire
unless expired? && @state != "expired"
apply ActionExecutionExpired.new(data: {
execution_id: @id,
expired_at: Time.now
})
end
end

def expired?
@started_at && Time.now - @started_at > EXPIRATION_TIME_IN_SECONDS
end

def valid_nonce?(nonce)
@nonce == nonce && !expired?
@nonce == nonce
end

private
Expand All @@ -84,17 +65,11 @@ def valid_nonce?(nonce)
@action_type = event.data[:action_type]
@data = event.data[:start_data] || {}
@nonce = event.data[:nonce]
@started_at = event.data[:started_at]
end

on ActionExecutionCompleted do |event|
@state = "completed"
@data.merge!(event.data[:completion_data])
end

on ActionExecutionExpired do |event|
@state = "expired"
@expired_at = event.data[:expired_at]
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,4 @@ class ActionExecutionCompleted < Infra::Event
attribute :execution_id, Infra::Types::UUID
attribute :completion_data, Infra::Types::Hash
end

class ActionExecutionExpired < Infra::Event
attribute :execution_id, Infra::Types::UUID
attribute :expired_at, Infra::Types::Time
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ def handle_complete_action_execution(command)
execution.complete(command.nonce, command.completion_data)
end
end

def handle_expire_action_execution(command)
@repository.with_aggregate(ActionExecution, command.aggregate_id) do |execution|
execution.expire if execution.expired?
end
end
end

class UnknownCommandError < StandardError; end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,29 @@
require_relative "action_strategy"
module ActionTracking
class GitcoinScoreActionStrategy < ActionStrategy
private

def action_type
"gitcoin_score"
end

def description
"Complete Gitcoin Passport verification"
end

def action_data(data)
{min_score: 20}
end
GITCOIN_SCORE_HUMANITY_THRESHOLD = 20

def start_execution(data)
user_id = data[:user_id]
wallet_address = data[:wallet_address]

response = api.request_message(user_id, wallet_address)
response = api.get_signing_message

{
state: "message_requested",
state: "started",
step: 0,
stepCount: 1,
nonce: response["nonce"],
message: response["message"]
}
end

def complete_execution(data)
wallet_address = data[:wallet_address]
signature = data[:signature]
wallet_address = data["address"]
signature = data["signature"]
nonce = data["nonce"]

response = api.submit_signature(wallet_address, signature)
score = response["score"]
response = api.submit_passport(wallet_address, signature, nonce)
score = response["score"].to_i

if score > 20
if score > GITCOIN_SCORE_HUMANITY_THRESHOLD
{
status: "completed",
state: "done",
Expand All @@ -52,16 +40,30 @@ def complete_execution(data)
end
end

private

def action_type
"gitcoin_score"
end

def description
"Complete Gitcoin Passport verification"
end

def action_data(data)
{min_score: GITCOIN_SCORE_HUMANITY_THRESHOLD}
end

def verify_completion(data)
data[:score] > 20
data[:score] > GITCOIN_SCORE_HUMANITY_THRESHOLD
end

private

def api
@api ||= begin
gitcoin_keys = Rails.application.credentials.gitcoin_api
GitcoinPassport.new(gitcoin_keys.api_key, gitcoin_keys.scorer_id)
GitcoinPassportApi.new(gitcoin_keys.api_key, gitcoin_keys.scorer_id)
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# govquests/action_tracking/test/strategy/gitcoin_score_action_strategy_test.rb

require_relative "../test_helper"
require "minitest/mock"

module ActionTracking
class GitcoinScoreActionStrategyTest < Test
def setup
super
@strategy = GitcoinScoreActionStrategy.new
@api_mock = Minitest::Mock.new
@strategy.instance_variable_set(:@api, @api_mock)
end

def test_action_type
assert_equal "gitcoin_score", @strategy.send(:action_type)
end

def test_description
assert_equal "Complete Gitcoin Passport verification", @strategy.send(:description)
end

def test_action_data
assert_equal({min_score: 20}, @strategy.send(:action_data, {}))
assert_equal({min_score: 30}, @strategy.send(:action_data, {min_score: 30}))
end

def test_start_execution
@api_mock.expect :get_signing_message, {"nonce" => "test_nonce", "message" => "test_message"}

result = @strategy.start_execution({})

assert_equal({
state: "message_requested",
nonce: "test_nonce",
message: "test_message"
}, result)

@api_mock.verify
end

def test_complete_execution_success
@api_mock.expect :submit_passport, {"score" => 25}, ["0x123", "signature", "nonce"]

result = @strategy.complete_execution({
address: "0x123",
signature: "signature",
nonce: "nonce"
})

assert_equal({
status: "completed",
state: "done",
score: 25,
message: "Gitcoin Passport verified successfully"
}, result)

@api_mock.verify
end

def test_complete_execution_failure
@api_mock.expect :submit_passport, {"score" => 15}, ["0x123", "signature", "nonce"]

result = @strategy.complete_execution({
address: "0x123",
signature: "signature",
nonce: "nonce"
})

assert_equal({
status: "failed",
state: "done",
score: 15,
message: "Gitcoin Passport score too low"
}, result)

@api_mock.verify
end

def test_verify_completion
assert @strategy.send(:verify_completion, {score: 25})
refute @strategy.send(:verify_completion, {score: 15})
end
end
end
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
# {
# "data": {
# "id": 2,
# "execution_id": "46b0fe20-dabc-45dd-bfc1-5e0f56d627e2",
# "action_id": "54d24d70-1c89-4a0a-88ad-e24a1926a2f3",
# "user_id": "1da1cdab-a037-4d92-8625-717c5fad9f35",
# "started_at": "2024-10-07T19:11:40.356Z",
# "status": "started",
# "created_at": "2024-10-07T19:11:40.402Z",
# "updated_at": "2024-10-07T19:11:40.402Z",
# "action_type": "gitcoin_score",
# "result": null,
# "completed_at": null,
# "nonce": "20f961d38281f14381a06dc9b69080f7",
# "start_data": {
# "step": 0,
# "nonce": "e8274b89c180a97dfb7268468484410b82572b0690fefd309d3131d8ab03",
# "state": "started",
# "message": "I hereby agree to submit my address in order to score my associated Gitcoin Passport from Ceramic.\n\nNonce: e8274b89c180a97dfb7268468484410b82572b0690fefd309d3131d8ab03\n",
# "stepCount": 1
# },
# "completion_data": {}
# },
# "nonce": "20f961d38281f14381a06dc9b69080f7",
# "execution_id": "46b0fe20-dabc-45dd-bfc1-5e0f56d627e2",
# "expires_at": "2024-10-07T19:41:40.356Z"
# }
class ActionExecutionBlueprint < Blueprinter::Base
fields :token, :execution_id, :expires_at
fields :execution_id, :start_data, :nonce, :completion_data, :action_type

identifier :execution_id
end
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
class ActionExecutionsController < ApplicationController
def start
result = ActionTracking::ActionExecutionService.start(
action_execution = ActionTracking::ActionExecutionService.start(
action_id: params[:action_id],
# TODO: user_id will be removed once we derive the user from the session
user_id: params[:user_id],
user_id: current_user.user_id,
start_data: params[:start_data]&.to_unsafe_h || {}
)

if result[:error]
render json: {error: result[:error]}, status: :not_found
if action_execution[:error]
render json: {error: action_execution[:error]}, status: :not_found
else
render json: result
render json: ActionExecutionBlueprint.render(action_execution)
end
end

def complete
result = ActionTracking::ActionExecutionService.complete(
action_execution = ActionTracking::ActionExecutionService.complete(
execution_id: params[:execution_id],
nonce: params[:nonce],
# TODO: user_id will be removed once we derive the user from the session
user_id: params[:user_id],
user_id: current_user.user_id,
completion_data: params[:completion_data]&.to_unsafe_h || {}
)

if result[:error]
render json: {error: result[:error]}, status: :unprocessable_entity
if action_execution[:error]
render json: {error: action_execution[:error]}, status: :unprocessable_entity
else
render json: result
render json: ActionExecutionBlueprint.render(action_execution)
end
end
end
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
class ApplicationController < ActionController::API
def current_user
@current_user ||= Authentication::UserReadModel.first
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ def submit_passport
end
end
end

# TODOS
# - first do e2e flow with gitcoin and a custom frontend page - DONE
# - then, connect this custom frontend page with the action tracking domain
# - then, connect the action tracking domain with the gitcoin passport scores controller
# - then, connect this with the actual frontend in an actual quest
# - then, write tests
Loading

0 comments on commit 87af4af

Please sign in to comment.