import { Web3Provider } from "@ethersproject/providers";
import { ethers } from "ethers";
import Web3 from "web3";
import { CommonAPI, UserAPI } from "../core/apis";
import {
RegisteredUserData,
UserApiInput,
UserDataResponse,
UserType,
UserWalletApi,
} from "../types";
import { EnvTypes } from "../typesBundle";
import { ETH_REQUEST_ACCOUNTS } from "../utils/Constants";
import { SIGN_MESSAGE } from "./CommonModule";
import { APIResponseType } from "../types/APIResponseType";
const StarkwareLib = require("@starkware-industries/starkware-crypto-utils");
const keyDerivation = StarkwareLib.keyDerivation;
const METAMASK_MESSAGE_SIGNATURE = `Welcome to Myria!\n\nSelect 'Sign' to create and sign in to your Myria account.\n\nThis request will not trigger a blockchain transaction or cost any gas fees.\n\n`;
/**
* Create WalletManager instance object
* @class WalletManager
* @param {EnvTypes} env Environment types enum params (Ex: EnvTypes.DEV / EnvTypes.STAGING / EnvTypes.PREPROD / EnvTypes.PROD)
*/
export class WalletManager {
private rawProvider: any;
private web3Provider: Web3Provider;
private userApi: UserAPI;
private web3: Web3;
private commonApi: CommonAPI;
public accounts: string[];
public currentAccount: string;
constructor(env: EnvTypes) {
if (typeof Web3.givenProvider === undefined) {
throw new Error(
"Metamask is required to install. Please install the metamask"
);
}
this.userApi = new UserAPI(env);
this.commonApi = new CommonAPI(env);
this.rawProvider = Web3.givenProvider;
this.web3 = new Web3(this.rawProvider);
this.web3Provider = new ethers.providers.Web3Provider(this.rawProvider);
this.accounts = [];
this.currentAccount = "";
}
/**
* @private
* @description Allow to generate the signature for registration process with server time
* @param {string} serverTime milliseconds as string
* @returns {string} a string as the message for metamask's signing
*/
private generateSignatureAccount(serverTime: string): string {
return `${METAMASK_MESSAGE_SIGNATURE}${JSON.stringify({
created_on: serverTime,
})}`;
}
/**
* @private
* @description Perform the register account through account services
* @param walletAddress
* @returns {UserWalletApi} Return user's wallet API response
*/
private async registerAccount(
walletAddress: string,
userType?: string,
referrerId?: string
): Promise<APIResponseType<UserWalletApi>> {
if (!walletAddress) {
throw new Error("Wallet address is required");
}
if(userType && !referrerId) {
throw new Error('ReferrerId is required!');
}
const serverTime = await this.commonApi.getTimeFromMyriaverse();
const message = this.generateSignatureAccount(serverTime.data.time);
const signature = await this.web3.eth.personal.sign(
message,
walletAddress,
""
);
const registerData: UserWalletApi = {
wallet_id: this.currentAccount,
signature,
message,
userType,
referrerId
};
const registerResponse = await this.commonApi.registerUser(registerData);
return registerResponse;
}
/**
* @private
* @description Register user in L2 system
* @param
* userType: Partner/Customer
* referrerID: starkKey/projectID-gameID
* @returns User data information in L2
*/
private async registerL2User(userType?: UserType, referrerId?: string): Promise<UserDataResponse | undefined> {
if (userType && !referrerId) {
throw new Error('ReferrerId is required!');
}
const msgHash = StarkwareLib.pedersen([
"UserRegistration:",
this.currentAccount,
]);
const walletSignature = await this.web3.eth.personal.sign(
SIGN_MESSAGE,
this.currentAccount,
""
);
const privateStarkKeyInternal = keyDerivation.getPrivateKeyFromEthSignature(walletSignature);
const keyPair = StarkwareLib.ec.keyFromPrivate(
privateStarkKeyInternal,
"hex"
);
const starkKey = keyDerivation.privateToStarkKey(privateStarkKeyInternal);
const pubKey = StarkwareLib.ec.keyFromPublic(
keyPair.getPublic(true, "hex"),
"hex"
);
const pureStarkSignature = StarkwareLib.sign(keyPair, msgHash);
const verify = StarkwareLib.verify(pubKey, msgHash, pureStarkSignature);
if (!verify) {
throw new Error(
"Stark signature generate error - please recheck the data"
);
}
const starkSignature = {
r: `0x${pureStarkSignature.r.toJSON()}`,
s: `0x${pureStarkSignature.s.toJSON()}`,
};
const payload: UserApiInput = {
ethAddress: this.currentAccount,
signature: starkSignature,
starkKey: `0x${starkKey}`,
userType,
referrerId
};
const registerResult = await this.userApi.registerUser(payload);
return registerResult.data;
}
/**
* @description Perform the connection with current connected metamask account with browser's web session
* @returns {RegisteredUserData} Return the new users (wallet address, details info for wallet)
* @throws {string} Exception: Need to select and connect to metamask accounts
* @example <caption>Sample code</caption>
*
// Sample code on staging:
const walletManager = new WalletManager(EnvTypes.STAGING);
const data = await walletManager.connect();
console.log('Data ->', data);
// Sample code on Production:
const walletManager = new WalletManager(EnvTypes.PRODUCTION);
const data = await walletManager.connect();
console.log('Data ->', data);
*/
public async connect(): Promise<RegisteredUserData> {
this.accounts = await this.web3Provider.send(ETH_REQUEST_ACCOUNTS, []);
this.currentAccount = this.accounts[0];
if (this.accounts.length === 0) {
throw new Error("Need to select and connect to metamask accounts");
}
let codeInfo;
let checkUserExistResponse;
try {
checkUserExistResponse = await this.userApi.getUserByWalletAddress(
this.currentAccount
);
codeInfo = "USER_REGISTERED";
} catch (err: any) {
if (err.status === 404) {
codeInfo = "USER_NOT_REGISTERED";
} else {
codeInfo = "GET_USER_INFO_ERROR";
}
}
const result: RegisteredUserData = {
walletAddress: this.currentAccount,
starkKey: checkUserExistResponse?.data.starkKey || "",
codeInfo,
};
return result;
}
/**
* @description The function required the wallet, and it performs full registration and normal login to get the user info data
* @param {string} walletAddress Required metamask wallet address of user
* @param {UserType=} userType Type of user for B2B (PARTNER) and B2C (CUSTOMER)
* @param {string=} referrerId Referrer users to onboard to myria system.
* In case the type is partner, then the referrerID is PARTNER_GAME_NAME_ID or PROJECT_ID
* If type is customer, then the referrerID is stark key of referred's user
* @throws {string} Exception: Wallet address is required!
* @throws {string} Exception: ReferrerId is required!
* @throws {string} Exception: Wallet registration failed: ${INTERNAL_SERVER_ERROR}
* @throws {string} Exception: Can't register the user with error: ${INTERNAL_SERVER_ERROR}
* @example <caption>Sample code</caption>
*
// Sample code on staging:
const walletManager = new WalletManager(EnvTypes.STAGING);
const data = await walletManager.registerAndLoginWithWalletAddress(
'0xfb.....', // wallet address
UserType.PARTNER, // User type as partner
'110', // Testnet (Staging) Project ID for the partner game
);
console.log('Testnet Data ->', data);
// Sample code on Production:
const walletManager = new WalletManager(EnvTypes.PRODUCTION);
const data = await walletManager.registerAndLoginWithWalletAddress(
'0xfb.....', // wallet address
UserType.PARTNER, // User type as partner
'10', // Production Project ID for the partner game
);
console.log('Production Data ->', data);
* @returns {UserDataResponse|undefined} User data response (such as stark key, wallet address, registered signature)
*/
public async registerAndLoginWithWalletAddress(walletAddress: string, userType?: UserType, referrerId?: string): Promise<UserDataResponse | undefined> {
if(!walletAddress) {
throw new Error('Wallet address is required!');
}
if(userType && !referrerId) {
throw new Error('ReferrerId is required!');
}
try {
const loginResult = await this.userApi.getUserByWalletAddress(
walletAddress
);
return loginResult?.data;
} catch (err: any) {
if (err.status === 404) {
const registerRes = await this.registerAccount(this.currentAccount, userType?.toString(), referrerId);
if (registerRes.status === "success") {
const registerUserData = await this.registerL2User();
return registerUserData;
}
else {
throw new Error(`Wallet registration failed: ${registerRes.data}`);
}
} else {
throw new Error(`Can't register the user with error: ${err}`);
}
}
}
/**
* @description Perform end to end registration process with metamask connection , connect wallet action and register user in Myria's system
* @param {UserType=} userType Type of user (PARTNER / CUSTOMER)
* @param {string=} referrerId Game_ID / Project_ID / References Stark Key of another users if userType is customer
* @example <caption>Sample code</caption>
*
// Sample code on staging:
const walletManager = new WalletManager(EnvTypes.STAGING);
const registerUserData = await walletManager.connectAndLogin(
UserType.PARTNER,
'110', // Testnet (Staging) Project ID for the partner game
);
console.log('Testnet Data ->', registerUserData);
// Sample code on Production:
const walletManager = new WalletManager(EnvTypes.PRODUCTION);
const registerUserData = await walletManager.connectAndLogin(
UserType.PARTNER,
'10', // Production Project ID for the partner game
);
console.log('Production Data ->', registerUserData);
* @returns {UserDataResponse|undefined} The details user data response for registration progress (including signature, stark key, wallet address)
* @throws {string} Exception: ReferrerId is required!
* @throws {string} Exception: Wallet registration failed: ${INTERNAL_SERVER_ERROR}
* @throws {string} Exception: Register user failed: ${INTERNAL_SERVER_ERROR}
* @throws {string} Exception: Cannot get user information with error: ${INTERNAL_SERVER_ERROR}
*/
public async connectAndLogin(userType?: UserType, referrerId?: string): Promise<UserDataResponse | undefined> {
this.accounts = await this.web3Provider.send(ETH_REQUEST_ACCOUNTS, []);
this.currentAccount = this.accounts[0];
if (userType && !referrerId) {
throw new Error('ReferrerId is required!');
}
try {
const loginResult = await this.userApi.getUserByWalletAddress(
this.currentAccount
);
return loginResult.data;
} catch (err: any) {
if (err.status === 404) {
try {
const registerRes = await this.registerAccount(this.currentAccount, userType, referrerId);
if (registerRes.status === "success") {
const registerResult = await this.registerL2User(userType, referrerId);
return registerResult;
} else {
throw new Error(`Wallet registration failed: ${registerRes.data}`);
}
} catch (error: any) {
throw new Error(`Register user failed: ${error}`);
}
} else {
throw new Error(`Cannot get user information with error: ${err}`);
}
}
}
/**
* @description Perform the retrieve user wallet by the Stark Key
* @param {string} starkKey Stark Key of user in L2 system of Myria
* @example <caption>Sample code</caption>
*
// Sample code on staging:
const walletManager = new WalletManager(EnvTypes.STAGING);
const starkKey = '0x.....';
const userWalletData = await walletManager.getUserWalletByStarkKey(starkKey);
console.log('Testnet Data ->', userWalletData);
// Sample code on Production:
const walletManager = new WalletManager(EnvTypes.PRODUCTION);
const starkKey = '0x.....';
const userWalletData = await walletManager.getUserWalletByStarkKey(starkKey);
console.log('Production Data ->', userWalletData);
* @returns {UserDataResponse | undefined} The details user data response for registration progress (including signature, stark key, wallet address)
* @throws {string} Exception: Stark Key is required!
* @throws {string} Http Status Code 404: User 0x... is not registered
* @throws {string} Http Status Code 500: Get user data failed - unexpected with internal server error
* @throws {string} Http Status Code 500: Internal Server Error with ${Exception}
*/
public async getUserWalletByStarkKey(starkKey: string): Promise<UserDataResponse | undefined> {
if (!starkKey) {
throw new Error("Stark Key is required")
}
let res: UserDataResponse;
try {
const registerUserResponse = await this.userApi.getUserByWalletAddress(starkKey);
if (registerUserResponse?.status === 'success' && registerUserResponse?.data) {
res = registerUserResponse?.data;
} else {
throw new Error('Get user data failed - unexpected with internal server error')
}
} catch (err: any) {
throw new Error('Internal Server Error with ' + err);
}
return res;
}
/**
* @description Perform the retrieve full user information by the Wallet address
* @param {string} ethAddress The ether wallet address of user (such as Metamask wallet address)
* @example <caption>Sample code</caption>
*
// Sample code on staging:
const walletManager = new WalletManager(EnvTypes.STAGING);
const ethWalletAddress = '0x.....';
const userWalletData = await walletManager.getUserInfoByWalletAddress(ethWalletAddress);
console.log('Testnet Data ->', userWalletData);
// Sample code on Production:
const walletManager = new WalletManager(EnvTypes.PRODUCTION);
const ethWalletAddress = '0x.....';
const userWalletData = await walletManager.getUserInfoByWalletAddress(ethWalletAddress);
console.log('Production Data ->', userWalletData);
* @returns {UserDataResponse | undefined} The details user data response for registration progress (including signature, stark key, wallet address)
* @throws {string} Exception: Eth address is required!
* @throws {string} Http Status Code 404: User 0x... is not registered
* @throws {string} Http Status Code 500: Get user data failed - unexpected with internal server error
* @throws {string} Http Status Code 500: Internal Server Error with ${Exception}
*/
public async getUserInfoByWalletAddress(ethAddress: string): Promise<UserDataResponse | undefined> {
if(!ethAddress) {
throw new Error("Eth address is required!");
}
let res: UserDataResponse;
try {
const registerUserResponse = await this.userApi.getUserByWalletAddress(ethAddress);
if (registerUserResponse?.status === 'success' && registerUserResponse?.data) {
res = registerUserResponse?.data;
} else {
throw new Error('Get user data failed - unexpected with internal server error')
}
} catch (err: any) {
throw new Error('Internal Server Error with ' + err);
}
return res;
}
}
Source