diff --git a/packages/utils/src/signer/SignerKeyringManager.ts b/packages/utils/src/signer/SignerKeyringManager.ts new file mode 100644 index 00000000..ea81f021 --- /dev/null +++ b/packages/utils/src/signer/SignerKeyringManager.ts @@ -0,0 +1,81 @@ +import { Keyring } from '@polkadot/api' +import { hexToU8a, stringToU8a, u8aToHex, u8aToString } from '@polkadot/util' +import { keccakAsU8a, naclDecrypt, naclEncrypt } from '@polkadot/util-crypto' +import { toSubsocialAddress } from '../accounts' + +class SignerKeyringManager { + private readonly keyring: Keyring + + constructor() { + this.keyring = new Keyring({ type: 'sr25519' }) + } + + private generateSalt() { + // has to be 32 bytes, otherwise naclEncrypt will throw an error + const SALT_LENGTH = 32 + const arr = new Uint8Array(SALT_LENGTH) + const arrayResult = crypto.getRandomValues(arr) + + return arrayResult + } + + private generateSecret(password: string, inputSaltStr?: string) { + const salt = inputSaltStr ? hexToU8a(inputSaltStr) : this.generateSalt() + const saltStr = u8aToHex(salt) + const secret = keccakAsU8a(saltStr + password) + + return { + saltStr, + secret, + } + } + + private generateNonce() { + const HEX_CONSTANT = '0x80001f' + return Buffer.from(HEX_CONSTANT.padEnd(24, '\0')) + } + + public async generateAccount(seed: string) { + const { sr25519PairFromSeed, mnemonicToMiniSecret } = await import('@polkadot/util-crypto') + + const miniSecret = mnemonicToMiniSecret(seed) + const { publicKey: publicKeyBuffer } = sr25519PairFromSeed(miniSecret) + + const publicKey = u8aToHex(publicKeyBuffer) + const secretKey = u8aToHex(miniSecret) + + return { publicAddress: toSubsocialAddress(publicKey)!, secretKey } + } + + public encryptKey(key: string, password: string) { + const messagePreEncryption = stringToU8a(key) + + const { saltStr, secret } = this.generateSecret(password) + + // use a static nonce + const NONCE = this.generateNonce() + + const { encrypted } = naclEncrypt(messagePreEncryption, secret, NONCE) + + const encryptedMessage = u8aToHex(encrypted) + + return { encryptedMessage, saltStr } + } + + public decryptKey(encryptedMessage: string, saltStr: string, password: string) { + const { secret } = this.generateSecret(password, saltStr) + const message = hexToU8a(encryptedMessage) + const NONCE = this.generateNonce() + + const decrypted = naclDecrypt(message, NONCE, secret) + + return u8aToString(decrypted) + } + + public generateKeypairBySecret(secret: string) { + const keypair = this.keyring.addFromUri(secret, {}, 'sr25519') + return keypair + } +} + +export default SignerKeyringManager \ No newline at end of file diff --git a/packages/utils/src/signer/api/request.ts b/packages/utils/src/signer/api/request.ts new file mode 100644 index 00000000..e704d336 --- /dev/null +++ b/packages/utils/src/signer/api/request.ts @@ -0,0 +1,203 @@ +import { AxiosError } from 'axios' +import { OffchainSignerEndpoint, sendHttpRequest, SendHttpRequestProps } from './utils' + +type EmailSignUpProps = { + email: string + password: string + accountAddress: string + signedProof: string + proof: string + hcaptchaResponse: string +} + +type EmailSignInProps = { + email: string + password: string + hcaptchaResponse: string +} + +type AddressSignInProps = { + signedProof: string + proof: string + hcaptchaResponse: string +} + +type ConfirmEmailProps = { + code: string + accessToken: string +} + +type SubmitSignedCallDataProps = { + data: string + jwt: string +} + +export type JwtPayload = { + accountAddress: string + accountAddressVerified: boolean + email: string + emailVerified: boolean + iat: number + exp: number +} +interface Error { + message: string[] + statusCode: number +} + +type ErrorMessageProps = { + error: unknown + showError?: boolean +} + +export function onErrorHandler( + error: unknown, + callback: (errorMessage: string) => void, + showError?: boolean, +) { + const errorMessage = getErrorMessage({ error, showError }) + if (errorMessage) { + callback(errorMessage as string) + } +} + +export function getErrorMessage({ error, showError = false }: ErrorMessageProps) { + const err = error as AxiosError + if (!err.response?.data) return err.message + const { message, statusCode } = err.response?.data + + // Handle specific error message from resend confirmation endpoint + if ( + statusCode === 400 && + typeof message === 'string' && + message === 'Wait before sending another confirmation.' && + !showError + ) + return + + return err.response?.data?.message ?? err.message +} + +export const requestProof = async (accountAddress: string) => { + const data = { + accountAddress, + } + + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.GENERATE_PROOF, + data, + }, + method: 'POST', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to generate proof', + }) + + return res +} + +export const addressSignIn = async (props: AddressSignInProps) => { + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.ADDRESS_SIGN_IN, + data: props, + }, + method: 'POST', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to sign in with address', + }) + + return res +} + +export const emailSignUp = async (props: EmailSignUpProps) => { + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.SIGNUP, + data: props, + }, + method: 'POST', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to sign up with email', + }) + + return res +} + +export const confirmEmail = async ({ code, ...otherProps }: ConfirmEmailProps) => { + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.CONFIRM, + data: { + code, + }, + }, + method: 'POST', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to confirm email', + ...otherProps, + }) + + return res +} + +export const resendEmailConfirmation = async (accessToken: string) => { + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.RESEND_CONFIRMATION, + }, + accessToken, + method: 'POST', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to resend email confirmation', + }) + + return res +} + +export const emailSignIn = async (props: EmailSignInProps) => { + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.SIGNIN, + data: props, + }, + method: 'POST', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to sign in with email', + }) + + return res +} + +export const fetchMainProxyAddress = async (accessToken: string) => { + const res = await sendHttpRequest({ + params: { + url: OffchainSignerEndpoint.FETCH_MAIN_PROXY, + }, + accessToken, + method: 'GET', + onFaileReturnedValue: undefined, + onFailedText: 'Failed to fetch proxy address', + }) + + return res +} + +export const submitSignedCallData = async ({ data, jwt }: SubmitSignedCallDataProps) => { + const payload: SendHttpRequestProps = { + params: { + url: OffchainSignerEndpoint.SIGNER_SIGN, + data: { + unsigedCall: data, + }, + }, + method: 'POST', + accessToken: jwt, + onFaileReturnedValue: undefined, + onFailedText: 'Failed submitting signed call data', + } + + const res = sendHttpRequest(payload) + + return res +} diff --git a/packages/utils/src/signer/api/utils.ts b/packages/utils/src/signer/api/utils.ts new file mode 100644 index 00000000..08571a65 --- /dev/null +++ b/packages/utils/src/signer/api/utils.ts @@ -0,0 +1,111 @@ +import { newLogger } from '../../logger' +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios' + +const log = newLogger('OffchainSignerRequests') + +export const OffchainSignerEndpoint = { + GENERATE_PROOF: 'auth/generate-address-verification-proof', + ADDRESS_SIGN_IN: 'auth/address-sign-in', + REFRESH_TOKEN: 'auth/refresh-token', + REVOKE_TOKEN: 'auth/revoke-token', + SIGNUP: 'auth/email-sign-up', + SIGNIN: 'auth/email-sign-in', + CONFIRM: 'auth/confirm-email', + RESEND_CONFIRMATION: 'auth/resend-email-confirmation', + SIGNER_SIGN: 'signer/sign', + FETCH_MAIN_PROXY: 'signer/main-proxy-address', +} as const + +export type OffchainSignerEndpoint = + typeof OffchainSignerEndpoint[keyof typeof OffchainSignerEndpoint] + +export type Method = 'GET' | 'POST' + +export const setAuthOnRequest = (accessToken: string) => { + try { + axios.interceptors.request.use( + async (config: AxiosRequestConfig) => { + config.headers = config.headers ?? {} + + config.headers = { + Authorization: accessToken, + } + + return config + }, + error => { + return Promise.reject(error) + }, + ) + } catch (err) { + log.error('Failed setting auth header', err) + } +} + +type SendRequestProps = { + request: () => Promise> + onFailedText: string + onFaileReturnedValue?: any +} + +export const sendRequest = async ({ request, onFailedText }: SendRequestProps) => { + try { + const res = await request() + if (res.status !== 200) { + console.warn(onFailedText) + } + + return res.data + } catch (err) { + return Promise.reject(err) + } +} + +type GetParams = { + url: string + baseUrl?: string + data?: any + config?: any +} + +export type SendHttpRequestProps = { + params: GetParams + onFaileReturnedValue: any + onFailedText: string + method: Method + accessToken?: string +} + +export const setBaseUrl = (rootUrl: string) => { + axios.defaults.baseURL = rootUrl +} + +export const sendHttpRequest = ({ + params: { url, baseUrl, data, config }, + method, + ...props +}: SendHttpRequestProps) => { + if (props.accessToken) setAuthOnRequest(props.accessToken) + + if (!axios.defaults.baseURL) throw new Error('Base URL is not set.') + + if (baseUrl) setBaseUrl(baseUrl) + + switch (method) { + case 'GET': { + return sendRequest({ + request: () => axios.get(url, config), + ...props, + }) + } + case 'POST': { + return sendRequest({ + request: () => axios.post(url, data, config), + ...props, + }) + } + default: { + return + } + } +} \ No newline at end of file