This SDK allows easy integration of Sequence Embedded Wallet from any Unreal Framework project.
https://docs.sequence.xyz/sdk/unreal/introduction
Unreal 5.2*, 5.3*, and 5.4 (* Supported by Beta 1.0.3 and lower)
- Android
- iOS
- PC standalone
- Mac standalone
This plugin requires the modern xcode setting to be enabled to build on MacOS. See the following link for more details.
IF you are using release Beta_1_0_3 or older please backup the values you stored in PluginConfig/Config.h
or Config/Config.h
After you've backed up your configuration data, Delete the entirety of the SequencePlugin directory. And drop
in the new updated version.
We now are opting to use .ini files to store configurations for the plugin rather than storing them in the plugin itself. This will make integrating updates to the plugin much simpler.
To do this please go to [YourProjectDirectory]/Config And create a file named [SequenceConfig.ini]
Within [SequenceConfig.ini] add the following lines:
[/Script/Sequence.Config]
FallbackEncryptionKey = ""
WaaSConfigKey = ""
ProjectAccessKey = ""
GoogleClientID = ""
AppleClientID = ""
FacebookClientID = ""
DiscordClientID = ""
RedirectUrl = "https://api.sequence.app"
PlayFabTitleID = ""
Here is where you'll fill in the various configuration values for the plugin. For the time being we don't support Facebook or Discord authentication so feel free to ignore those 2 clientId's for now.
The WaaSTenantKey value in the SequenceConfig.ini has been changed to WaaSConfigKey if you do not update this value in the SequenceConfig.ini, the SequencePlugin will be unable to load your settings.
If you are installing the plugin from the Epic game store the process is slightly different. First you'll actually want to install the plugin into the engine from the store. Once this is done you'll open up a project, Go to Edit/Plugins/Sequence/SequencePlugin. You'll need to enable the plugin as it won't be enabled by default. The engine will need to restart before you'll be able to see the plugin content.
The second thing you'll need to be aware of is the location of the SequencePluginContent folder. Because it's installed as an engine plugin, you'll need to enable engine content in the content drawer to be able to find it. The location will be All/Engine/Plugins/SequencePluginContent/Core, within the content drawer.
If your project is currently on 5.2 or 5.3, then for this version you'll want to upgrade to 5.4. To do this close the Unreal Editor, Close your Code Editor. Right-click on your Unreal Project file. Then select Switch Unreal Engine Version, Choose 5.4 and click okay. Once done. Open your code editor and rebuild your source code.
As a way of future proofing we recommend installing visual studio 2022 in accordance with these docs as Unreal 5.4 and onward will be dropping support for visual studio 2019.
In some instances your build.cs may not update properly for your project when this happens you'll be unable to import plugin, if this occurs in your Projects Build.cs file please check the private Dependency module such that it includes "SequencePlugin":
public SequenceTest(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" });
//The line below is what you want
PrivateDependencyModuleNames.AddRange(new string[] { "SequencePlugin" });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your project file with the Enabled attribute set to true
}
Before you can use this plugin, you need to acquire the following credentials from Sequence
WaaSConfigKey
ProjectAccessKey
You can then add these credentials in the [SequenceConfig.ini] file under [YourProject]/Config/SequenceConfig.ini
Before you can use PlayFab you'll need to create a game with PlayFab Here Then in the SequencePluginConfig.ini, set the PlayFabTitleID String to the TitleID of your game.
You must provide a 32 character encryption key in the [SequenceConfig.ini] file under [YourProject]/Config/SequenceConfig.ini
under the config variable FallbackEncryptionKey
In order to prevent tampering with data you must encrypt your packaged project using Unreals packaging settings You can refer to these docs
Note: For generating secure keys feel free to use tools such as: Lastpass Password generator or equivalent, Encryption Key Generators, etc
Be sure what ever key data you place in [FallbackEncryptionKey] is of hex form & does NOT contain any extraneous symbols, eg) *,/\ etc. In the event unrecognized symbols are seen the engine will not load the .ini file.
-
Once you have the
SequencePlugin
folder, you'll need to go to your project directory and create aPlugins
folder in it, then copy over theSequencePlugin
folder into thePlugins
folder. If aPlugins
folder already exists just copy theSequencePlugin
folder into it. -
Launch your project, then allow it to update the UProject Settings.
-
To find the
SequencePlugin
content folder in your content drawer enable view plugin content -
If you wish to use the in built sequence UI for login you have to do the following:
-
Create a blueprint Actor (if you wish to spawn it yourself) Or Pawn (if you wish to use it with a Gamemode)
-
Attach the [AC_SequencePawn_Component] to the Blueprint you created via the add components section.
Where to find Components window
- Then you can setup your blueprint like so to start utilizing the SequenceAPI
Note: Auth Success Forwarder will let you know when the system is ready to be used
- Depending on what you choose your blueprint parent class to be, You can do one of two things to finish this process. If it's a pawn or a subclass of a pawn, you can attach it to your Gamemode so that it spawns when play begins, OR you can drag it out into your scene if it's just an actor Blueprint.
-
Create a C++ Class that Inherits from [Pawn] (If you wish to use it with Gamemode) or [Actor] (If you'll be spawning it yourself) If you don't know how to do this refer to the doc Creating C++ Classes in Unreal, for the purpose of these docs I'll refer to the C++ Class created here as the [C++ Parent]
-
In [C++ Parent] .h file include the Header [SequenceAPI.h] this will allow you to access the [USequenceWallet]
-
Create a BlueprintCallable function within the [C++ Parent]
-
Create a Blueprint that inherits from [C++ Parent], Then Attach the following Actor component to it [AC_SequencePawn_Component]. For in depth specifics on how to setup this blueprint please refer to the demonstration BP Graph Image, this is the BP Graph of [BP_CustomSpectatorPawn] contained within the plugins content folder, & serves as a template for your own Blueprint graph.
- For those who aren't familiar with Unreal's Blueprint system you can create a blueprint by right clicking in the content
drawer, then click blueprint class. Within the blueprint class selector select the All Classes dropdown & search
for your [C++ Parent] class you just made.
Note: You can simply duplicate the [BP_CustomSpectatorPawn] (if you do this be sure to move the duplicate outside of the plugin folder into YOUR content folder, Otherwise your work could be lost during an update to the plugin).
- If your [C++ Parent] was a pawn, you can set it to be the default pawn in your Gamemode and it will spawn on BeginPlay, If your [C++ Parent] was an Actor, you can manually add it to the scene. In both cases the UI will show up on BeginPlay.
Note: If you don't know how to modify / update the Gamemode / Gamemode settings go to ProjectSettings -> Maps & Modes, From there you can set the Gamemode and update the default pawn. Or in the case you wish to use our Gamemode for testing it's [GM_Sequence] You'll just need to set your pawn as the default pawn.
If you don't know what some of the Entities referred to above are / how they work in unreal please refer to the following Docs: To learn more about GameModes and GameMode state refer to these docs To learn more about Pawns refer to these docs Pawns Components PlayerController UI in Unreal C++ & Blueprints Creating C++ Classes in Unreal
This isn't the only way you can setup the Builtin GUI, this is here as a quick start reference for those just getting started with unreal.
In the folder located at All/Plugins/SequencePlugin Content/Core/Style you'll find a struct F_SequenceUIStyle, In the default values section of this struct you'll be able to update the colours and images displayed throughout the UI. For beta we currently only read from Sequence_Style_Dark_Mode
In a C++ UObject with a series of pass through [UFUNCTIONS] setup similarly to [SequenceBackendManager.h/.cpp]. Each of these calls is implemented in [UAuthenticator] you just need to pass through the data with YOUR UAuthenticator UObject
/**
* Resets the IsFederatingSessionInUse State to false
* For cases where you don't want to Federate a session in use if the error
* occurs for "EmailAlreadyInUse"
*/
void ResetFederateSessionInUse();
/**
* Sets a custom encryptor
* @param EncryptorIn Encryptor to use
*/
void SetCustomEncryptor(UGenericNativeEncryptor * EncryptorIn);
/**
* Used to get an OIDC Login Url
* @param Type Type of OIDC Url need
* @return An OIDC login Url of the specified Type
*/
FString GetSigninURL(const ESocialSigninType& Type) const;
/**
* Used to initiate mobile Login
* @param Type Type of OIDC to conduct on Mobile
* @param ForceCreateAccountIn Force create account if it already exists
*/
void InitiateMobileSSO(const ESocialSigninType& Type, const bool ForceCreateAccountIn);
/**
* Internal Mobile Login call. Used to complete mobile login once a tokenized URL is received
* @param TokenizedUrl The URL containing an IdToken
*/
void UpdateMobileLogin(const FString& TokenizedUrl);
/**
* Internal Mobile Login Call. Used to complete mobile Login once an IdToken has been received
* @param IdTokenIn IdToken Received from mobile login
*/
void UpdateMobileLogin_IdToken(const FString& IdTokenIn);
/**
* Used to initiate OIDC login
* @param IDTokenIn OIDC Token granted from login
* @param ForceCreateAccountIn Force create account if it already exists
*/
void SocialLogin(const FString& IDTokenIn, const bool ForceCreateAccountIn);
/**
* Used to initiate email login
* @param EmailIn Email
* @param ForceCreateAccountIn Force create account if it already exists
*/
void EmailLogin(const FString& EmailIn, const bool ForceCreateAccountIn);
/**
* Used to login as a Guest into Sequence
* @param ForceCreateAccountIn Force create account if it already exists
*/
void GuestLogin(const bool ForceCreateAccountIn) const;
/**
* Used to create & login a new account with PlayFab, Then OpenSession with Sequence
* @param UsernameIn Username
* @param EmailIn Email
* @param PasswordIn Password
* @param ForceCreateAccountIn Force create account if it already exists
*/
void PlayFabRegisterAndLogin(const FString& UsernameIn, const FString& EmailIn, const FString& PasswordIn, const bool ForceCreateAccountIn);
/**
* Used to login with PlayFab, Then OpenSession with Sequence
* @param UsernameIn Username
* @param PasswordIn Password
* @param ForceCreateAccountIn Force create account if it already exists
*/
void PlayFabLogin(const FString& UsernameIn, const FString& PasswordIn, const bool ForceCreateAccountIn);
/**
* Used to complete Email based authentication, whether it be for normal Authentication OR Federation
* @param CodeIn Received Code from email
*/
void EmailLoginCode(const FString& CodeIn);
/**
* Used To Federate an Email (WIP)
* @param EmailIn Email to federate
*/
void FederateEmail(const FString& EmailIn);
/**
* Used to Federate an OIDC Login
* @param IdTokenIn OIDC Token To federate
*/
void FederateOIDCIdToken(const FString& IdTokenIn);
/**
* Used to initiate OIDC account federation on mobile
* @param Type Type of OIDC account to federate
*/
void InitiateMobileFederateOIDC(const ESocialSigninType& Type);
/**
* Used to federate a new PlayFab account
* @param UsernameIn PlayFab Username
* @param EmailIn PlayFab Email
* @param PasswordIn PlayFab Password
*/
void FederatePlayFabNewAccount(const FString& UsernameIn, const FString& EmailIn, const FString& PasswordIn) const;
/**
* Used to federate an existing account on PlayFab
* @param UsernameIn PlayFab Username
* @param PasswordIn PlayFab Password
*/
void FederatePlayFabLogin(const FString& UsernameIn, const FString& PasswordIn) const;
/**
* Used to force open the last failed OpenSession Attempt
*/
void ForceOpenLastOpenSessionAttempt() const;
/**
* Used to get stored credentials from Disk
* @return Stored Credentials
*/
FStoredCredentials_BE GetStoredCredentials() const;
/**
* Used to store Credentials on Disk
* @param Credentials Credentials to be Stored
*/
void StoreCredentials(const FCredentials_BE& Credentials) const;
/**
* Clears stored credentials on disk with blanks
*/
void ClearStoredCredentials() const;
To start you'll want to create a [UAuthenticator] UObject like so [UAuthenticator * Auth = NewObject()], this UObject manages the authentication side of Sequence.
Be sure to bind to the Delegates for [AuthSuccess], [AuthFailure], [AuthRequiresCode] prior to making any signin calls You can bind to these delegates like so:
this->Authenticator->AuthSuccess.AddDynamic(this, &ASequenceBackendManager::CallShowAuthSuccessScreen);
this->Authenticator->AuthRequiresCode.AddDynamic(this, &ASequenceBackendManager::CallReadyToReceiveCode);
this->Authenticator->AuthFailure.AddDynamic(this, &ASequenceBackendManager::CallShowAuthFailureScreen);
this->Authenticator->FederateSuccess.AddDynamic(this, &ASequenceBackendManager::CallShowFederationSuccess);
this->Authenticator->FederateFailure.AddDynamic(this, &ASequenceBackendManager::CallShowFederationFailure);
this->Authenticator->FederateOrForce.AddDynamic(this, &ASequenceBackendManager::CallShowFederateOrForce);
Note: Replace the usage of the SequenceBackendManager.h/.cpp with you're own when building a custom GUI,
it is only used here as a reference in the event more context is needed with these instructions.
Where [CallShowAuthSuccessScreen] is defined in SequenceBackendManager.h
as an example like so:
UFUNCTION()
void CallShowAuthSuccessScreen();
And in SequenceBackendManager.cpp
like so:
void ASequenceBackendManager::CallShowAuthSuccessScreen()
{
this->Credentials = CredentialsIn;
if (this->ShowAuthSuccessDelegate.IsBound())
this->ShowAuthSuccessDelegate.Broadcast();
else
UE_LOG(LogTemp, Error, TEXT("**[Nothing bound to: ShowAuthSuccessDelegate]**"));
}
-
To start email based authentication you'll start it with this call [EmailLogin(const FString& EmailIn)], supplying an email you've collected from the User in your GUI.
-
Next [AuthRequiresCode] will fire when the [UAuthenticator] is ready to receive the Code from your UI. Collect this code from your GUI and send it to the authenticator using [EmailCode(CodeIn)].
-
Finally [AuthSuccess] will fire with a Credentials_BE struct as a parameter. You are done Email Based Auth.
-
To start SSO based authentication with desktop you can either use your own implementation to get the necessary id_token or you can make use of Unreal's web browser plugin.
-
To get the URL to navigate to you can use the UAuthenticator supplied call [FString GetSigninURL(const ESocialSigninType& Type)] where Type is the social login type you wish to use
-
With whatever implementation you chose you can forward the collected id_token to the UAuthenticator object with [SocialLogin(const FString& IDTokenIn)], after which [AuthSuccess] will fire and you're done desktop based SSO.
- To start mobile SSO you will need to make use of the [UAuthenticator::InitiateMobileSSO(const ESocialSigninType& Type)] where type is the Type of SSO you want to use. IE) Google or Apple, for the time being Discord & Facebook aren't supported. This function call is all that's required for Mobile SSO.
- Start by calling either PlayFabLogin (Login With Existing) or PlayFabRegisterAndLogin (Create a new PlayFab account & Login with it) that's it.
- Start by calling GuestLogin, and that's it.
In cases where users sign in & all is well, you can allow users to federate other login types so long as the Email matches. Doing this allows users to login with Email, OIDC & Playfab login types such that they all access the same wallet address.
You can use the following calls to achieve this: "UAuthenticator::FederateEmail" , "UAuthenticator::FederateOIDCIdToken" , "UAuthenticator::InitiateMobileFederateOIDC" , "UAuthenticator::FederatePlayFabNewAccount" , "UAuthenticator::FederatePlayFabLogin"
In the case of FederateEmail you need to be bound to the AuthRequiresCode Delegate and you complete the call with "UAuthenticator::EmailLoginCode"
In cases where users attempt to signin but are already logged into another session with the same Email, you'll need to do the following.
First be sure the following delegates are bound: FederateSuccess , FederateFailure , FederateOrForce
FederateSuccess Fires when the Federation Operation Is Successful.
FederateFailure Fires when the Federation Operation Fails, It also includes the string Error of what went wrong.
FederateOrForce Fires when an Authentication attempt fails with EmailAlreadyInUse error.
When a user attempts to Signin and it fails with email already in use, FederateOrForce will fire. This delegate will include [FFederationSupportData], which contains 2 pieces of information, The email the user wishes to federate & a list of login types they are already signed in with. We have 2 options we can choose between:
-
Federate: With the ValidLoginTypes list, Present those options to the user as login methods to use, Being sure to specify that they must use the email that was also presented in [FFederationSupportData]. If they successfully login with one of those types, the system will automatically federate their account and no further action will be required.
-
ForceCreate: Ask the user if they'd wish to force create a new account with the email address present in [FFederationSupportData] & login type they initially tried. This will assign a new wallet address to them & it will be treated like an entirely separate account. To do this simply call "UAuthenticator::ForceOpenLastOpenSessionAttempt" This will ForceCreate a new account with the last login attempt that resulted in an EmailAlreadyInUse error.
Google:
In order to be able to properly use Google Auth, create and place the Keystore file by following these instructions.
You will also need to generate an [Android client ID] and a [Web Application client ID] for your application. And place the [Web Application client ID] in the [YourProject/Config/SequenceConfig.ini], [GoogleClientID] field.
Refer to these docs to generate [Android client ID] and [Web Application client ID].
This guide helps explain how to collect SHA-1 key fingerprints for the [Android client ID].
Apple: Please ensure you have a proper [AppleClientID] set in [YourProject/Config/SequenceConfig.ini]
Google: Please ensure you have a proper [GoogleClientID] set in [YourProject/Config/SequenceConfig.ini]
Apple: Please ensure you have a proper [AppleClientID] set in [YourProject/Config/SequenceConfig.ini], be sure you register and set your bundle identifier properly for your app
For Apple SSO to work please be sure to register the [RedirectUrl] in [YourProject/Config/SequenceConfig.ini] appropriately for your app.
In order to use the Blueprint Sequence API, you must create an actor or pawn and attach the [AC_SequencePawn_Component]
When setting values for ERC20 amounts you may try to set a value like 0.15 USDC in the ValueString of the FERC20Transaction struct, However this is NOT the value you put inside the Value String for ERC20 Transactions, The value you put in is 0.15 * 10^6. To simplify this process there are Util functions in Blueprints that you can use to put human readable values in like so.
Where the TokenBalance Structure is the same type that's contained in the return from the Indexer Call GetTokenBalances
In some cases the TokenBalance Struct won't have a decimals Value initiated properly. In which case you can actually hard code the decimals value with the following conversion function instead.
You can add the various transaction types to a Transactions UObject like so in blueprints.
Creating a Transactions Object & a ERC20 Type to it Where the DemoERC20 struct looks like
Creating a Transactions Object & a ERC721 Type to it Where the DemoERC721 struct looks like
Creating a Transactions Object & a ERC1155 Type to it Where the DemoERC1155 struct looks like
Creating a Transactions Object & a Raw Type to it Where the DemoRaw struct looks like
Creating a Transactions Object & a DelayedEncoding Type to it
You can go about creating the Arguments object like so
Get Supported Transak Countries
We have created a wrapper TypeDef for TUnion<FRawTransaction, FERC20Transaction, FERC721Transaction, FERC1155Transaction, FDelayedTransaction> called TransactionUnion, You should use the TransactionUnion DataType instead. This change also comes as we have introduced the Delayed Transaction Type.
In order to gain access to the SequenceAPI be sure to #include "Sequence/SequenceAPI.h" After you've completed initial authentication and have intercepted the credentials either through your UI or ours, to use the Sequence API you'll need to create a [USequenceWallet]* by using:
/*
Automatically tries to read stored credentials on disk and initialize with them
if none are found returns a TOptional<USequenceWallet*> Pointer without any set Credentials
*/
USequenceWallet::Get()
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Wallet = WalletOptional.GetValue();
//Use here
}
or
/*
returns a TOptional<USequenceWallet*> Pointer set with the
given Credentials
*/
USequenceWallet::Get(const FCredentials_BE& Credentials)
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get(Credentials);
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Wallet = WalletOptional.GetValue();
//Use here
}
or
/*
returns a TOptional<USequenceWallet*> Pointer set with the
given Credentials & ProviderUrl
*/
USequenceWallet::Get(const FCredentials_BE& Credentials,const FString& ProviderUrl);
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get(Credentials,"ProviderUrl");
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Wallet = WalletOptional.GetValue();
//Use here
}
Once you have your [USequenceWallet] you can feel free to call any of the functions Supplied by the object, as the register call is now automatically done for you.
USequenceWallet is now a Subsystem of GameInstance, what this means is not only can you access it from anywhere as if it was a static variable in your C++ code. But it also persists for the lifetime of your game. That is no data is reset when a level is changed in your games!
Used to convert a user readable amount of a token into a system usable one, IE) 1 USDC -> 1000000 (int64 representation of 1 USDC)
int64 SystemReadableAmount = USequenceWallet::GetSystemReadableAmount(0.01, 6);//0.01 USDC
Used to convert a system readable amount of a token into a user readable one IE) 1000000 (int64 representation of 1 USDC) -> 1 USDC
float UserReadableAmount = USequenceWallet::GetUserReadableAmount(1000000, 6);//1 USDC
const TFunction<void(TArray<FFeeOption>)> OnResponse = [=](const TArray<FFeeOption>& Response)
{
//Process filtered fee options
};
const FFailureCallback GenericFailure = [OnFailure](const FSequenceError& Error)
{
//Process Failure state
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
TArray<TransactionUnion> Transactions;
FERC20Transaction T20;
T20.to = "0x0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2";
T20.tokenAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
T20.value = "1000";
Transactions.Push(TransactionUnion(T20));
Api->GetFeeOptions(Transactions,OnResponse,GenericFailure);
}
const TFunction<void(TArray<FFeeOption>)> OnResponse = [=](const TArray<FFeeOption>& Response)
{
//Process fee options
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//Process Failure state
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
TArray<TransactionUnion> Transactions;
FERC20Transaction T20;
T20.to = "0x0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2";
T20.tokenAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
T20.value = "1000";
Transactions.Push(TransactionUnion(T20));
Api->GetUnfilteredFeeOptions(Transactions,OnResponse,GenericFailure);
}
TArray<TransactionUnion> Transactions;
FERC721Transaction T721;
T721.safe = true;
T721.id = "101424663676543340124133645812717438592241191887187111290563634379068086785120";
T721.to = "0x245b738089F1fa668D927422d2943F75A9e89819";
T721.tokenAddress = "0xa9a6a3626993d487d2dbda3173cf58ca1a9d9e9f";
Transactions.Push(TransactionUnion(T721));
const TFunction<void(TArray<FFeeOption>)> OnFeeResponse = [Transactions, OnSuccess, OnFailure](const TArray<FFeeOption>& Response)
{
if (Response.Num() > 0)
{
const FFeeOption SelectedFeeOption = Response[0];
const FFailureCallback OnTransactionFailure = [OnFailure](const FSequenceError& Error)
{
OnFailure("Transaction failure", Error);
};
const UAuthenticator * Auth = NewObject<UAuthenticator>();
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get(Auth->GetStoredCredentials().GetCredentials());
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->SendTransactionWithFeeOption(Transactions,SelectedFeeOption,[=](const FTransactionResponse& Transaction)
{
FString OutputString;
const TSharedRef< TJsonWriter<> > Writer = TJsonWriterFactory<>::Create(&OutputString);
FJsonSerializer::Serialize(Transaction.Json.ToSharedRef(), Writer);
OnSuccess(OutputString);
}, OnTransactionFailure);
}
}
else
{
OnFailure("Test failed no fee options in response",FSequenceError(EErrorType::EmptyResponse,"Empty fee option response"));
}
};
const FFailureCallback OnFeeFailure = [OnFailure](const FSequenceError& Error)
{
OnFailure("Get Fee Option Response failure", Error);
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->GetFeeOptions(Transactions,OnFeeResponse,OnFeeFailure);
}
const TFunction<void (TArray<FSupportedCountry>)> OnSuccess = [=](TArray<FSupportedCountry> SupportedCountries)
{
//Process success
};
const TFunction<void (FSequenceError)> OnFailure = [=]( const FSequenceError& Err)
{
//Process error
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->GetSupportedTransakCountries(OnSuccess,OnFailure);
}
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->OpenTransakLink();
}
const TSuccessCallback<FSignedMessage> OnResponse = [=] (const FSignedMessage& Response)
{
//Response is the signed message
};
const FFailureCallback OnFailure = [=](const FSequenceError& Error)
{
UE_LOG(LogTemp,Display,TEXT("Error Message: %s"),*Error.Message);
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
const FString Message = "Hi";
Api->SignMessage(Message,OnResponse,OnFailure);
}
Note: if you want to call contracts with the Raw type you'll want to include the header
#include "ABI/ABI.h"
in order to use the ABI to encode the data for a contract call.
const FFailureCallback OnFailure = [=](const FSequenceError& Error)
{
UE_LOG(LogTemp,Display,TEXT("Error Message: %s"),*Error.Message);
};
//Create the Transaction object list
TArray<TransactionUnion> Txn;
//Create the transactions you wish to perform
//ERC20
FERC20Transaction T20;
T20.to = "0x0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2";
T20.tokenAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174";
T20.value = "1000";
//ERC721
FERC721Transaction T721;
T721.safe = true;
T721.id = "54530968763798660137294927684252503703134533114052628080002308208148824588621";
T721.to = "0x0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2";
T721.tokenAddress = "0xa9a6A3626993D487d2Dbda3173cf58cA1a9D9e9f";
//ERC1155
FERC1155Transaction T1155;
T1155.to = "0x0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2";
T1155.tokenAddress = "0x631998e91476DA5B870D741192fc5Cbc55F5a52E";
FERC1155TxnValue Val;
Val.amount = "1";
Val.id = "66635";
T1155.vals.Add(Val);
//Raw (Example contract call)
FString FunctionSignature = "balanceOf(address,uint256)";
TFixedABIData Account = ABI::Address(FAddress::From("0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2"));
TFixedABIData Id = ABI::UInt32(0x01);
TArray<ABIEncodeable*> Arr;
Arr.Add(&Account);
Arr.Add(&Id);
FUnsizedData EncodedData = ABI::Encode(FunctionSignature, Arr);
FRawTransaction T;
T.data = "0x" + EncodedData.ToHex();
T.to = "0x64d9f9d527abe2a1c1ce3fada98601c4ac5bfdd2";
T.value = "0";
//Now append your transaction requests to the Txn object
Txn.Push(TransactionUnion(T20));//ERC20
Txn.Push(TransactionUnion(T721));//ERC721
Txn.Push(TransactionUnion(T1155));//ERC1155
Txn.Push(TransactionUnion(T));//ContractCall
//Now send the transaction
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->SendTransaction(Txn,[=](const FTransactionResponse& Transaction)
{
TSharedPtr<FJsonObject> Json = Transaction.Json;
TSharedPtr<FJsonObject> Receipt = Transaction.Receipt;
TSharedPtr<FJsonObject> NativeReceipt = Transaction.NativeReceipt;
TSharedPtr<FJsonObject> Request = Transaction.Request;
TArray<TSharedPtr<FJsonValue>> Simulations = Transaction.Simulations;
FString TxHash = Transaction.TxHash;
FString IdentifyingCode = Transaction.IdentifyingCode;
FString MetaTxHash = Transaction.MetaTxHash;
},OnFailure);
}
const TSuccessCallback<TArray<FSession>> OnSuccess = [=](TArray<FSession> Response)
{
//Response is a list of Sessions
};
const FFailureCallback OnFailure = [=](const FSequenceError& Error)
{
UE_LOG(LogTemp,Display,TEXT("Error Message: %s"),*Error.Message);
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->ListSessions(OnSuccess,OnFailure);
}
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->SignOut();
}
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->GetWalletAddress();
}
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->GetNetworkId();
}
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->UpdateNetworkId(137);
}
TArray<FIdNamePair> Networks = USequenceWallet::GetAllNetworks();
TArray<FString> NetworkNames = USequenceWallet::GetAllNetworkNames();
TArray<int64> NetworkIds = USequenceWallet::GetAllNetworkIds();
int64 NetworkId = USequenceWallet::GetNetworkId(TEXT("polygon"));
FString NetworkName = USequenceWallet::GetNetworkName(137);
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->UpdateProviderURL("NewProviderUrl");
}
The Delayed encoding TransactionType is added onto a TArray like any other type, However the way this transaction is built is very different from the others. This type is meant to allow you to send unencoded data to the server for it to encode for you into valid ethereum transaction.
There are 4 DataTypes you need to know about in order to use DelayedEncoding,
- FDelayedTransaction, This Data type houses all of the arguments for the encoding.
- UDelayedEncodingBP, This Object is used to represent the Data Portion of the FDelayedTransaction.
- UDelayedEncodingArrayArgsBP, This is one of two argument UObjects, This one is used for housing Array Args
- UDelayedEncodingObjectArgsBP, This is the other argument UObject, This one is used for housing Object Args.
The 2 Argument UObjects support full nesting, Meaning you can have an ArrayArg inside an ArrayArg, As well as an ObjectArg inside an ObjectArg. They also support nesting ObjectArgs in ArrayArgs and ArrayArgs in ObjectArgs. As well as the traditional JSON Primitives, Such as Bool, Number(float, double, int32, int64), String.
Below is an example of how to Construct an FDelayedTransaction with lots of nesting.
TArray<TransactionUnion> Txn;
FDelayedTransaction TDelayed;
TDelayed.data->SetFunc("setInt()");
TDelayed.data->SetAbi("Epic ABI");
UDelayedEncodingObjectArgsBP * NestedObj = NewObject<UDelayedEncodingObjectArgsBP>();
NestedObj->AddStringArg(TEXT("Deep String"), TEXT("Blah"));
UDelayedEncodingArrayArgsBP * NestedArg = NewObject<UDelayedEncodingArrayArgsBP>();
NestedArg->AddObjectArg(NestedObj);
UDelayedEncodingArrayArgsBP * ArgsListInner = NewObject<UDelayedEncodingArrayArgsBP>();
ArgsListInner->AddBoolArg(false);
ArgsListInner->AddInt32Arg(32);
ArgsListInner->AddInt64Arg(64);
ArgsListInner->AddStringArg(TEXT("String Arg"));
ArgsListInner->AddArrayArg(NestedArg);
UDelayedEncodingObjectArgsBP * ArgsMain = NewObject<UDelayedEncodingObjectArgsBP>();
ArgsMain->AddBoolArg(TEXT("Epic Boolean"), false);
ArgsMain->AddDoubleArg(TEXT("Epic Double Arg"), 0.1);
ArgsMain->AddInt64Arg(TEXT("Epic integer64 arg"), -1);
ArgsMain->AddArrayArg(TEXT("List Arg"),ArgsListInner);
TDelayed.data->SetArgs(ArgsMain);
TDelayed.to = "0x245b738089F1fa668D927422d2943F75A9e89819";
TDelayed.value = "0";
Txn.Push(TransactionUnion(TDelayed));
The indexer is tied nicely with the wallet to allow for ease of use.
One thing to note is the NetworkId you set with your wallet is the one that will be used with
the indexer. The default network we set is 137
const TSuccessCallback<bool> GenericSuccess = [=](const bool bSuccess)
{
//Ping response is in bSuccess
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//Ping failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->Ping(GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqVersion> GenericSuccess = [=](const FSeqVersion& version)
{
//Response contained in FSeqVersion
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//Version Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->Version(GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqRuntimeStatus> GenericSuccess = [=](const FSeqRuntimeStatus& runTimeStatus)
{
//Response is in FSeqRunTimeStatus
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//RunTimeStatus Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->RunTimeStatus(GenericSuccess, GenericFailure);
}
const TSuccessCallback<int64> GenericSuccess = [=](const int64 chainID)
{
//Response in int64
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetChainID Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->GetChainID(GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqEtherBalance> GenericSuccess = [=](const FSeqEtherBalance& etherBalance)
{
//Response in FSeqEtherBalance
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetEtherBalance Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
Api->GetEtherBalance(Api->GetWalletAddress(), GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqGetTokenBalancesReturn> GenericSuccess = [=](const FSeqGetTokenBalancesReturn& tokenBalances)
{
//Response in FSeqGetTokenBalancesReturn
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetTokenBalances Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
FSeqGetTokenBalancesArgs args;
args.accountAddress = Api->GetWalletAddress();
args.includeMetaData = true;
Api->GetTokenBalances(args, GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqGetTokenSuppliesReturn> GenericSuccess = [=](const FSeqGetTokenSuppliesReturn& tokenSupplies)
{
//Response is in FSeqGetTokenSuppliesReturn
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetTokenSupplies Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
FSeqGetTokenSuppliesArgs args;
args.contractAddress = "0x01";//Testing Contract Address in hex with leading 0x
args.includeMetaData = true;
Api->GetTokenSupplies(args, GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqGetTokenSuppliesMapReturn> GenericSuccess = [=](const FSeqGetTokenSuppliesMapReturn& tokenSuppliesMap)
{
//Response is in FSeqGetTokenSuppliesMapReturn
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetTokenSuppliesMap Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
TMap<FString, FSeqTokenList> tokenMap;
const TPair<FString,FSeqTokenList> item;
tokenMap.Add(item);
FSeqGetTokenSuppliesMapArgs args;
args.includeMetaData = true;
args.tokenMap = tokenMap;
Api->GetTokenSuppliesMap(args, GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqGetBalanceUpdatesReturn> GenericSuccess = [=](const FSeqGetBalanceUpdatesReturn& balanceUpdates)
{
//Response in FSeqGetBalanceUpdatesReturn
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetBalanceUpdates Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
FSeqGetBalanceUpdatesArgs args;
args.contractAddress = "0x0E0f9d1c4BeF9f0B8a2D9D4c09529F260C7758A2";
args.page.page = 10;
args.page.more = true;
Api->GetBalanceUpdates(args, GenericSuccess, GenericFailure);
}
const TSuccessCallback<FSeqGetTransactionHistoryReturn> GenericSuccess = [=](const FSeqGetTransactionHistoryReturn& transactionHistory)
{
//Response is in FSeqGetTransactionHistoryReturn
};
const FFailureCallback GenericFailure = [=](const FSequenceError& Error)
{
//GetTransactionHistory Failure
};
const TOptional<USequenceWallet*> WalletOptional = USequenceWallet::Get();
if (WalletOptional.IsSet() && WalletOptional.GetValue())
{
USequenceWallet * Api = WalletOptional.GetValue();
FSeqGetTransactionHistoryArgs args;
args.filter.accountAddress = Api->GetWalletAddress();
args.includeMetaData = true;
args.page.page = 0;
args.page.more = true;
Api->GetTransactionHistory(args, GenericSuccess, GenericFailure);
}
Assuming you've setup your controlling Pawn with the [AC_SequencePawn_Component] The sequence pawn component has functions to do the following:
Setup Sequence (sets up the sequence based systems), requires playerController input
Show GUI Shows the UI
Hide GUI Hides the UI
GUI Visible Simple Visibility test for the UI
Switch Platform (Switches which mode the UI will be in and how it will be displayed)
Note: this doesn't rotate the application into any one view it just makes the UI responsive to that type of view.
Modes:
- Desktop (default)
- Mobile Portrait (Custom built for portrait mode reducing the X width wherever possible)
- Mobile Landscape
We make use of TFunctions with some callbacks:
TSuccessCallback
const TFunction<void(FString)> OnResponse = **[Capturable variables]**(const FString& Response)
{
//callback body where we can process Response
};
FFailureCallback
const TFunction<void(FSequenceError)> OnFailureTest = **[Capturable variables]**(const FSequenceError& Error)
{
//callback body where we can process Error
};
One thing to be aware of is to keep an eye on capturables if you have lots of nested TFunctions it's very easy to miss something and start over writing memory. If you require lots of nesting swapping to a better approach using UFUNCTION callbacks helps to avoid these problems.
Most users of the Sequence SDK will not need to interact with cryptographic functions directly.
We encapsulate binary data using the FBinaryData
structs, which is a wrapper around a pointer to a shared byte array TSharedPtr<TArray<uint8>>
.
Binary data is further subtyped into FUnsizedData
, which represents data of any variable size, and TSizedData<TSize>
, which represents data of a required byte length TSize
.
Important cryptographic types of set size, such as 32-byte private keys, are defined as subtypes of TSizedData
- for example, we define FPrivateKey : TSizedData<32>
.
These can also be loaded from hex strings using From(FString Str)
, such as FPrivateKey::From("0x0...0");
. Ensure that the input string is the correct size.
To call contracts on the blockchain, you will need to encode any data you wish to pass as arguments using the ABI. To read more about the ABI and its specification, check out the solidity docs.
Our ABI implementation centers around the ABI
class in ABI/ABI.h
, which provides functions to convert the following types: UInt32
, Int32
, Bool
, FAddress
, and FString
. Any other data may be transformed directly into TFixedABIArray
or TDynamicABIArray
for fixed-length and dynamic length arrays respectively, or to TFixedABIData
and TDynamicABIData
for fixed-length and dynamic length binary data.
Once you have your data stored in ABIEncodeable
types, you can provide the ABI an array of the type TArray<ABIEncodeable*>
to ABI::Encode
to receive the binary encoding of the arguments.
See TestABI.cpp
for an example.
Eth/Crypto.h
provides some important ethereum functions for interacting directly with the blockchain:
// Derives a public key from a private key
FPublicKey GetPublicKey(FPrivateKey PrivateKey);
// Derives an address from the public key
FAddress GetAddress(FPublicKey PublicKey);
// Finds a keccak hash for some binary data
FHash256 GetKeccakHash(FBinaryData &Data);
// Derives contract address from a given sending address and nonce
FAddress GetContractAddress(FAddress Sender, FBlockNonce Nonce);
EthTransaction.h
contains a struct designed for managing raw ethereum transactions, including functions to sign and hash them. Note that transactions should usually be handled via the Sequence wallet interface, which sends the transactions via the Sequence WAAS.
To set your system up for Packaging please refer to the following links:
- Windows and macOS
- Setting up Visual Studio for Unreal on Windows
- Android
- iOS
- Mac Specific Software Requirements
For iOS apps you also need to setup provisioning, following these docs
When setting up your project to build for Android you'll need to update the following settings: In ProjectSettings/Android SDK Set SDK API Level to Android-34 Set NDK API Level to anything in the range [26,33] (We personally used android-32)
For Hardware Requirements with Unreal please refer to these docs
During login on OSX it's likely users will be asked to login to their keychain if they aren't already. This is because we store a random encryption key in the Users keychain and it may require permission from the user to do so.
During the Unreal Package process in the event a code signing error occurs you can take the following steps within XCode to get your packaged .app file
- After packaging the project in Unreal, open the Xcode project (Sequence-unreal folder -> Intermediate -> ProjectFilesIOS -> SequenceUnreal.xcodeproj)
- Click on the project name on the left hand side to open up project settings
- Click the Build Phase Tab
- Click on the ‘+’ icon at the top left
- Select Run Script
- Drag the new run script to one below from the last item in the phase list
- Expand the run script
- In the script box, add the following command:
xattr -cr /[path-to-your-project]/[your-project-name]/Binaries/IOS/Payload/[your-project-name.app]
- Click on the Build Settings tab
- Click on each item under the Architectures header that contains macOS and hit the delete key
- Click on the General tab
- Click on Mac and Applevision Pro under supported destinations and hit the delete key
- Now the project can be built (if the build fails at first, wait a few moments then try again. It can sometimes take a bit before the build registers the run script)
- Once you have finished running the project, and want to make changes to the code, REMEMBER to delete this xcodeproj file in the sequence-unreal folder to ensure that a new xcodeproj is created when you package the project again._
In events where the editor crashes on launch, try deleting the following file, Saved/SaveGames/Cr.sav This file contains the savegame data for credentials and if modified can result in an engine crash. Deleting this file deletes local credentials, so you'll need to log back in.