From cea584b3559543d76852d9303b5298445810ce05 Mon Sep 17 00:00:00 2001 From: Nik Ho Date: Thu, 3 Oct 2024 19:20:52 +1300 Subject: [PATCH] feat: add sign typed data v4 method --- ...assportZkEvmSignTypedDataV4AsyncAction.cpp | 52 ++++++ .../Private/Immutable/ImmutablePassport.cpp | 60 +++++++ ...lPassportZkEvmSignTypedDataV4AsyncAction.h | 36 ++++ .../Public/Immutable/ImmutableDataTypes.h | 9 + .../Public/Immutable/ImmutableNames.h | 1 + .../Public/Immutable/ImmutablePassport.h | 7 + .../Public/Immutable/ImmutableRequests.h | 160 ++++++++++++++++++ 7 files changed, 325 insertions(+) create mode 100644 Source/Immutable/Private/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.cpp create mode 100644 Source/Immutable/Public/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.h diff --git a/Source/Immutable/Private/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.cpp b/Source/Immutable/Private/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.cpp new file mode 100644 index 0000000..bd2b86d --- /dev/null +++ b/Source/Immutable/Private/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.cpp @@ -0,0 +1,52 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.h" + +#include "Immutable/ImmutablePassport.h" +#include "Immutable/ImmutableSubsystem.h" +#include "Immutable/Misc/ImtblLogging.h" + +UImtblPassportZkEvmSignTypedDataV4AsyncAction* UImtblPassportZkEvmSignTypedDataV4AsyncAction::ZkEvmSignTypedDataV4(UObject* WorldContextObject, const FZkEvmSignTypedDataV4Request& Request) +{ + UImtblPassportZkEvmSignTypedDataV4AsyncAction* PassportZkEvmSignTypedDataV4BlueprintNode = NewObject(); + PassportZkEvmSignTypedDataV4BlueprintNode->WorldContextObject = WorldContextObject; + PassportZkEvmSignTypedDataV4BlueprintNode->SignRequest = Request; + return PassportZkEvmSignTypedDataV4BlueprintNode; +} + +void UImtblPassportZkEvmSignTypedDataV4AsyncAction::Activate() +{ + if (!WorldContextObject || !WorldContextObject->GetWorld()) + { + FString Err = "zkEVM Sign Typed Data V4 failed due to missing world or world " "context object."; + IMTBL_WARN("%s", *Err) + Failed.Broadcast(Err, TEXT("")); + return; + } + + GetSubsystem()->WhenReady(this, &UImtblPassportZkEvmSignTypedDataV4AsyncAction::DoZkEvmSignTypedDataV4); +} + +void UImtblPassportZkEvmSignTypedDataV4AsyncAction::DoZkEvmSignTypedDataV4(TWeakObjectPtr JSConnector) +{ + auto Passport = GetSubsystem()->GetPassport(); + + if (Passport.IsValid()) + { + Passport->ZkEvmSignTypedDataV4(SignRequest, UImmutablePassport::FImtblPassportResponseDelegate::CreateUObject(this, &UImtblPassportZkEvmSignTypedDataV4AsyncAction::OnZkEvmSignTypedDataV4Response)); + } +} + +void UImtblPassportZkEvmSignTypedDataV4AsyncAction::OnZkEvmSignTypedDataV4Response(FImmutablePassportResult Result) +{ + if (Result.Success) + { + IMTBL_LOG("zkEVM Sign Typed Data V4 success") + MessageSigned.Broadcast(TEXT(""), UImmutablePassport::GetResponseResultAsString(Result.Response)); + } + else + { + IMTBL_LOG("zkEVM Sign Typed Data V4 failed") + Failed.Broadcast(Result.Error, TEXT("")); + } +} diff --git a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp index ff6c4ab..6d38ccc 100644 --- a/Source/Immutable/Private/Immutable/ImmutablePassport.cpp +++ b/Source/Immutable/Private/Immutable/ImmutablePassport.cpp @@ -10,6 +10,7 @@ #include "Immutable/ImmutableSaveGame.h" #include "Kismet/GameplayStatics.h" #include "Policies/CondensedJsonPrintPolicy.h" +#include "Json.h" #if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC #include "GenericPlatform/GenericPlatformHttp.h" @@ -27,6 +28,34 @@ #define PASSPORT_SAVE_GAME_SLOT_NAME TEXT("Immutable") +TSharedPtr UStructToJsonObject(const UStruct* Struct, const void* StructData) +{ + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + + if (FJsonObjectConverter::UStructToJsonObject(Struct, StructData, JsonObject.ToSharedRef(), 0, 0)) + { + return JsonObject; + } + + return nullptr; // Return nullptr if conversion fails +} + +TArray> ConvertNameTypeToJsonArray(const TArray& NameTypes) +{ + TArray> NameTypeArray; + + for (const FZkEvmSignTypedDataV4NameType& Item : NameTypes) + { + TSharedPtr NameTypeItemObject = MakeShareable(new FJsonObject()); + NameTypeItemObject->SetStringField("name", Item.Name); + NameTypeItemObject->SetStringField("type", Item.Type); + + NameTypeArray.Add(MakeShareable(new FJsonValueObject(NameTypeItemObject))); + } + + return NameTypeArray; +} + void UImmutablePassport::Initialize(const FImmutablePassportInitData& Data, const FImtblPassportResponseDelegate& ResponseDelegate) { check(JSConnector.IsValid()); @@ -130,6 +159,37 @@ void UImmutablePassport::ZkEvmGetTransactionReceipt(const FZkEvmTransactionRecei CallJS(ImmutablePassportAction::ZkEvmGetTransactionReceipt, UStructToJsonString(Request), ResponseDelegate, FImtblJSResponseDelegate::CreateUObject(this, &UImmutablePassport::OnBridgeCallbackResponse)); } +void UImmutablePassport::ZkEvmSignTypedDataV4(const FZkEvmSignTypedDataV4Request& Request, const FImtblPassportResponseDelegate& ResponseDelegate) +{ + // this messy and manual JSON conversion is because the UR4 JSON util automatically downcases the first letter of + // property names. The SignTypedDataV4 method on the game bridge side requires the `Request.types` properties + // to be capitalised, otherwise the TS Passport module will throw a validation erro. + TSharedPtr DomainJsonObject = UStructToJsonObject(FZkEvmSignTypedDataV4Domain::StaticStruct(), &Request.domain); + TSharedPtr MessageJsonObject = UStructToJsonObject(FZkEvmSignTypedDataV4Message::StaticStruct(), &Request.message); + + TArray> OrderComponentsArray = ConvertNameTypeToJsonArray(Request.types.OrderComponents); + TArray> ConsiderationItemArray = ConvertNameTypeToJsonArray(Request.types.ConsiderationItem); + TArray> OfferItemArray = ConvertNameTypeToJsonArray(Request.types.OfferItem); + TArray> EIP712DomainArray = ConvertNameTypeToJsonArray(Request.types.EIP712Domain); + TSharedPtr TypesJsonObject = MakeShareable(new FJsonObject()); + TypesJsonObject->SetArrayField("OrderComponents", OrderComponentsArray); + TypesJsonObject->SetArrayField("ConsiderationItem", ConsiderationItemArray); + TypesJsonObject->SetArrayField("OfferItem", OfferItemArray); + TypesJsonObject->SetArrayField("EIP712Domain", EIP712DomainArray); + + TSharedPtr JsonObject = MakeShareable(new FJsonObject()); + JsonObject->SetObjectField("domain", DomainJsonObject); + JsonObject->SetObjectField("types", TypesJsonObject); + JsonObject->SetObjectField("message", MessageJsonObject); + JsonObject->SetStringField("primaryType", Request.primaryType); + + FString JsonString; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&JsonString); + FJsonSerializer::Serialize(JsonObject.ToSharedRef(), Writer); + + CallJS(ImmutablePassportAction::ZkEvmSignTypedDataV4, JsonString, ResponseDelegate, FImtblJSResponseDelegate::CreateUObject(this, &UImmutablePassport::OnBridgeCallbackResponse)); +} + void UImmutablePassport::ConfirmCode(const FString& DeviceCode, const float Interval, const FImtblPassportResponseDelegate& ResponseDelegate) { FImmutablePassportCodeConfirmRequestData Data{DeviceCode, Interval}; diff --git a/Source/Immutable/Public/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.h b/Source/Immutable/Public/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.h new file mode 100644 index 0000000..72aaa01 --- /dev/null +++ b/Source/Immutable/Public/Immutable/Actions/ImtblPassportZkEvmSignTypedDataV4AsyncAction.h @@ -0,0 +1,36 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Immutable/ImmutablePassport.h" +#include "ImtblBlueprintAsyncAction.h" +#include "ImtblPassportZkEvmSignTypedDataV4AsyncAction.generated.h" + +/** + * Async action blueprint node for zkEVM Send Transaction + */ +UCLASS() +class IMMUTABLE_API UImtblPassportZkEvmSignTypedDataV4AsyncAction : public UImtblBlueprintAsyncAction +{ + GENERATED_BODY() + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPassportZkEvmSignTypedDataV4OutputPin, FString, ErrorMessage, FString, Signature); + +public: + UFUNCTION(BlueprintCallable, meta = (WorldContext = "WorldContextObject", BlueprintInternalUseOnly = "true"), Category = "Immutable") + static UImtblPassportZkEvmSignTypedDataV4AsyncAction* ZkEvmSignTypedDataV4(UObject* WorldContextObject, const FZkEvmSignTypedDataV4Request& Request); + + virtual void Activate() override; + +private: + FZkEvmSignTypedDataV4Request SignRequest; + + UPROPERTY(BlueprintAssignable) + FPassportZkEvmSignTypedDataV4OutputPin MessageSigned; + UPROPERTY(BlueprintAssignable) + FPassportZkEvmSignTypedDataV4OutputPin Failed; + + void DoZkEvmSignTypedDataV4(TWeakObjectPtr JSGetConnector); + void OnZkEvmSignTypedDataV4Response(FImmutablePassportResult Result); +}; diff --git a/Source/Immutable/Public/Immutable/ImmutableDataTypes.h b/Source/Immutable/Public/Immutable/ImmutableDataTypes.h index 46a0b54..530014b 100644 --- a/Source/Immutable/Public/Immutable/ImmutableDataTypes.h +++ b/Source/Immutable/Public/Immutable/ImmutableDataTypes.h @@ -276,3 +276,12 @@ struct IMMUTABLE_API FZkEvmTransactionReceipt UPROPERTY() FString type; }; + +USTRUCT(BlueprintType) +struct FZkEvmSignTypedDataV4Response +{ + GENERATED_BODY() + + UPROPERTY() + FString signature; +}; diff --git a/Source/Immutable/Public/Immutable/ImmutableNames.h b/Source/Immutable/Public/Immutable/ImmutableNames.h index 8e87a6c..7183e94 100644 --- a/Source/Immutable/Public/Immutable/ImmutableNames.h +++ b/Source/Immutable/Public/Immutable/ImmutableNames.h @@ -16,6 +16,7 @@ namespace ImmutablePassportAction const FString ZkEvmSendTransaction = TEXT("zkEvmSendTransaction"); const FString zkEvmSendTransactionWithConfirmation = TEXT("zkEvmSendTransactionWithConfirmation"); const FString ZkEvmGetTransactionReceipt = TEXT("zkEvmGetTransactionReceipt"); + const FString ZkEvmSignTypedDataV4 = TEXT("zkEvmSignTypedDataV4"); #if PLATFORM_ANDROID | PLATFORM_IOS | PLATFORM_MAC const FString GetPKCEAuthUrl = TEXT("getPKCEAuthUrl"); diff --git a/Source/Immutable/Public/Immutable/ImmutablePassport.h b/Source/Immutable/Public/Immutable/ImmutablePassport.h index adde8fb..26ba04b 100644 --- a/Source/Immutable/Public/Immutable/ImmutablePassport.h +++ b/Source/Immutable/Public/Immutable/ImmutablePassport.h @@ -115,6 +115,13 @@ class IMMUTABLE_API UImmutablePassport : public UObject * FImtblPassportResponseDelegate to call on response from JS. */ void ZkEvmGetTransactionReceipt(const FZkEvmTransactionReceiptRequest& Request, const FImtblPassportResponseDelegate& ResponseDelegate); + + /** + * Generate a signature for a typed data V4 object + * @param Request Type data to sign + * @param ResponseDelegate The response delegate of type FImtblPassportResponseDelegate to call on response from JS. + */ + void ZkEvmSignTypedDataV4(const FZkEvmSignTypedDataV4Request& Request, const FImtblPassportResponseDelegate& ResponseDelegate); void GetIdToken(const FImtblPassportResponseDelegate& ResponseDelegate); void GetAccessToken(const FImtblPassportResponseDelegate& ResponseDelegate); diff --git a/Source/Immutable/Public/Immutable/ImmutableRequests.h b/Source/Immutable/Public/Immutable/ImmutableRequests.h index b969603..b4f09a1 100644 --- a/Source/Immutable/Public/Immutable/ImmutableRequests.h +++ b/Source/Immutable/Public/Immutable/ImmutableRequests.h @@ -62,3 +62,163 @@ struct IMMUTABLE_API FZkEvmTransactionReceiptRequest UPROPERTY() FString txHash; }; + +USTRUCT(BlueprintType) +struct IMMUTABLE_API FZkEvmSignTypedDataV4Domain +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString ChainId; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Name; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString VerifyingContract; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Version; +}; + +USTRUCT(BlueprintType) +struct FZkEvmSignTypedDataV4NameType +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Name; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Type; +}; + +USTRUCT(BlueprintType) +struct FZkEvmSignTypedDataV4SignableMessageTypes +{ + GENERATED_BODY() + + // JsonFieldName metadata specifier is used to define the exact name that should be used in the JSON output + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (JsonFieldName = "OrderComponents")) + TArray OrderComponents; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (JsonFieldName = "OfferItem")) + TArray OfferItem; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (JsonFieldName = "ConsiderationItem")) + TArray ConsiderationItem; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (JsonFieldName = "EIP712Domain")) + TArray EIP712Domain; +}; + +USTRUCT(BlueprintType) +struct FZkEvmSignTypedDataV4OfferItem +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + uint8 ItemType; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Token; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString IdentifierOrCriteria; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString StartAmount; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString EndAmount; +}; + +USTRUCT(BlueprintType) +struct FZkEvmSignTypedDataV4ConsiderationItem +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + uint8 ItemType; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Token; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString IdentifierOrCriteria; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString StartAmount; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString EndAmount; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Recipient; +}; + +USTRUCT(BlueprintType) +struct FZkEvmSignTypedDataV4Message +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Offerer; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Zone; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + TArray Offer; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + TArray Consideration; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + uint8 OrderType; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString StartTime; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString EndTime; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString ZoneHash; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Salt; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString ConduitKey; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString Counter; +}; + +USTRUCT(BlueprintType) +struct IMMUTABLE_API FZkEvmSignTypedDataV4Request +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FZkEvmSignTypedDataV4Domain domain; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FZkEvmSignTypedDataV4SignableMessageTypes types; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FZkEvmSignTypedDataV4Message message; + + UPROPERTY(BlueprintReadWrite, EditAnywhere) + FString primaryType; +}; + +// USTRUCT(BlueprintType) +// struct IMMUTABLE_API FZkEvmSignTypedDataV4Request +// { +// GENERATED_BODY() +// +// UPROPERTY(BlueprintReadWrite, EditAnywhere) +// FSignableMessage message; +// }; \ No newline at end of file