@hinkal/react-hooks v0.0.5
Hinkal React SDK
The User Flow
A new user has to go through two stages before he is able to do shielded transactions:
- Sign-in
- Getting an access token through KYC process
In the sign-in stage, the user needs to sign a message "Login To Hinkal".
The signature will be used to generate a pair of shielded private and public keys.
Only the user should know the shielded private key to sign shielded transactions,
whereas the shielded public key will be used in order to receive shielded payments from other members.
useSignIn
hook gives the connectWallet
function which is used in order to walk a user through the sign-in process.
The second stage for the new user involves going through the KYC process and obtaining an access token
in order to be able to execute shielded transactions. useKyc
hook gives a set of callbacks that are designed to facilitate a user journey through the KYC process.
If a user already has an access token, then no additional actions are required.
After passing the two stages above, a user is able to access shielded transactions:
- deposit
- transfer
- withdraw
- swap
- deposit with Meson
Sign In
This section provides sign-in functionality to Hinkal Protocol.
Description
useSignIn
hook is used in order for a user to get his shielded public and private keys,
which are used in order to cryptographically sign shielded transactions and receive shielded payments. Hinkal protocol prompts the user for an Ethereum Signed Message and an optional password used to derive the shieled keys associated with a Hinkal Account.
In addition, useSignIn
hook sets change_network
and change_account
listeners which are used to recalculate shielded balances
if the user changes either a network or an account.
Types of Hinkal accounts
Hinkal Protocol supports two types of accounts:
Normal account
For this type of account, the shielded keys are calculated from an Ethereum Signed Message signed with the user's EOA
Password-protected account
For this type of account, the shielded keys are calculated from an Ethereum Signed Message signed with the user's EOA in conjuction with a password string.
We recommend to use this type of account to improve users' security against phishing attacks.
Minimal usage
The sign in flow is a 3-step process. To let these steps run in the background, a combined signIn
function can be used.
The signIn
function accepts a provider object as an argument and asks the user to sign a message which is used to derive his shielded keys.
If a password is provided to the useSignIn
hook, it is configured in the hinkal instance to derive the password-protected account, otherwise a normal account is configured.
const { signIn } = useSignIn({
password: 'MyHinkalPassword1337!',
onSuccess: () => {
console.log('Hinkal account connected!');
},
onError: () => {
console.log('Connection error.');
},
});
await signIn(provider);
Advanced usage
If you want to enhance the user experience by providing such features as password-checks and a richer UI, you can use all of the sign in steps in a sequence.
initHinkal
In this step, a Hinkal instance is initiated by connecting to a provider and prompting the user for an Ethereum Signed Message.
fetchHinkalAccounts
The cache of blockchain logs is checked to determine if the user has created a password-protected account in the past.
The cached data represents a keccak256 hash of the user's password-protected shieldedAddress, with a salt derived from the user's signed message.
const cachedUserData = ethers.utils.solidityKeccak256( ['bytes', 'uint256'], [shieldedAddress, ethers.BigNumber.from(ethers.utils.keccak256(hinkalSignedMessage))], );
connectHinkalUser
In this step, a provided password is checked against the retrieved onchain logs. If no information about the user is retrieved, a
register
call is sent to the Hinkal smart contract. After that the final settings are applied to the Hinkal instance and the sign in process is finished.
enum SignInState {
NOT_CONNECTED,
FETCHING_ACCOUNTS,
EXISTING_USER,
NEW_USER,
SUCCESS,
}
const [password, setPassword] = useState<string>('');
const [signInState, setSignInState] = (useState<SignInState> = SignInState.NOT_CONNECTED);
const { initHinkal, fetchHinkalUser, connectHinkalUser, checkHinkalPassword, isRecurringHinkalUser } = useSignIn({
password,
initHinkalCallback: {
onSuccess: () => {
fetchHinkalUser?.();
setSignInState(SignInState.FETCHING_ACCOUNTS);
},
onError: () => {
console.log('Init step error.');
},
},
fetchHinkalUserCallback: {
onSuccess: () => {
if (isRecurringHinkalUser) {
setSignInState(SignInState.EXISTING_USER);
return;
}
setSignInState(SignInState.NEW_USER);
},
onError: () => {
console.log('Cached user check error.');
},
},
connectHinkalUserCallback: {
onSuccess: () => {
setSignInState(SignInState.SUCCESS);
},
onError: () => {
console.log('Connection step error.');
},
},
});
switch (signInState) {
case SignInState.NOT_CONNECTED:
await initHinkal(provider);
return;
case SignInState.FETCHING_ACCOUNTS:
displayLoader();
return;
case SignInState.EXISTING_USER:
const usingPassword = promptForPasswordUsage();
if (usingPassword) {
setPassword(promptUserForPassword());
const passwordCorrect = checkHinkalPassword();
if (passwordCorrect) {
await connectHinkalUser();
return;
}
displayPasswordIncorrectMessage();
return;
}
setPassword('');
await connectHinkalUser();
return;
case SignInState.NEW_USER:
const usingPassword = promptForPasswordUsage();
if (usingPassword) {
setPassword(promptUserForPassword());
await connectHinkalUser();
return;
}
setPassword('');
await connectHinkalUser();
return;
case SignInState.SUCESS:
displaySuccessMessage();
return;
}
API Interface
interface HinkalCallback {
onError?: (err: Error) => void;
onSuccess?: () => void;
}
interface SignInCallback extends HinkalCallback {
onChainChanged?: (chainId: string) => void;
onAccountChanged?: () => void;
}
interface UseSignInParams extends SignInCallback {
password?: string;
initHinkalCallback?: HinkalCallback;
fetchHinkalUserCallback?: HinkalCallback;
connectHinkalUserCallback?: HinkalCallback;
}
type UseSignInResult {
signIn: () => void,
isConnecting: boolean,
isFetching: boolean,
initHinkal: () => void,
fetchHinkalUser: () => void,
connectHinkalUser: () => void,
checkHinkalPassword: () => void,
isRecurringHinkalUser: boolean,
hasNormalAccount: boolean,
}
function useSignIn({
password,
onError,
onSuccess,
onChainChanged,
onAccountChanged,
initHinkalCallback,
fetchHinkalUserCallback,
connectHinkalUserCallback
}: UseSignInParams): UseSignInResult;
KYC hook
This section gives an overview of a KYC status management in Hinkal Protocol.
useKyc
hook encapsulates the whole KYC logic of Hinkal Protocol including user state management and user information forms.
aiPrise is an external provider of KYC flow used in the Hinkal SDK.
There are four stages of KYC process:
- Not-Started. The user did not start the KYC process yet.
- Started. The user is in process of inputting KYC data.
- Processing. KYC information is sent to KYC provider and is being processed.
- Finished. The KYC process has finished and there is an outcome: either success or failure.
useKyc
accepts callbacks that are fired after the success or failure events of some user actions.
onKycStatusChanged
callback is invoked after the KYC status of the user has changed, it has a kycStatus argument that shows updated information.
onAiPriseSuccess
and onAiPriseError
callbacks govern actions after the success and failure events of opening aiPrise window with user information inputs.
onMintSuccess
and onMintError
callbacks fire in case of successful minting of access token or failure to mint one.
enabled
variable controls whether the user should proceed to KYC input form in case he does not possess an access token.
function useKyc({
enabled,
onKycStatusChanged,
onAiPriseSuccess,
onAiPriseFinished,
onAiPriseError,
onMintSuccess,
onMintError,
}: UseKycResult);
type UseKycParams = {
enabled?: boolean;
onKycStatusChanged?: (kycStatus?: KycStatus) => void;
onAiPriseSuccess?: () => void;
onAiPriseFinished?: () => void;
onAiPriseError?: (err: Error) => void;
onMintSuccess?: () => void;
onMintError?: (err: Error) => void;
};
type UseKycResult = {
kycStatus?: KycStatus;
openAiPrise: () => void;
isAiPriseProcessing: boolean;
mintToken?: () => void;
isMintTokenProcessing: boolean;
mintingTransactionStatus: TransactionStatus;
closeMinting: () => void;
};
Shielded Transactions
This section contains the main functions a user interacts with Hinkal smart contract. All functions listed are shielded transactions.
1. Deposit
The deposit
function allows Ether and ERC-20 deposits to Hinkal smart contract.
After the deposit, the shielded balance of a user will be credited with the deposit amount.
useDeposit
hook accepts two optional callbacks for success and for error.
The hook returns the deposit
function and isProcessing
boolean variable which shows whether the deposit action is currently in process.
The deposit
function accepts the token object and the deposit amount.
function useDeposit({ onError, onSuccess }: UseDepositParams): UseDepositReturn;
type UseDepositParams = {
onError?: (err: Error) => void;
onSuccess?: () => void;
};
type UseDepositResult = {
isProcessing: boolean;
deposit?: (token: ERC20Token, depositAmount: string) => void;
};
2. Transfer
The transfer
function allows shielded transfers between two shielded addresses.
During the transfer, neither the transfer amount nor the recipient address is revealed on-chain.
Transfer does not involve the invocation of ERC-20 transfer function, instead, UTXO changes its owner.
The useTransfer
hook returns the transfer
function which accepts the token object, transfer amount, and the transfer address.
The latter is a shielded address of the recipient.
function useTransfer({ onError, onSuccess }: UseTransferParams): UseTransferResult;
type UseTransferParams = {
onError?: (err: Error) => void;
onSuccess?: () => void;
};
type UseTransferResult = {
isProcessing: boolean;
transfer?: (token: ERC20Token, transferAmount: string, transferAddress: string) => void;
};
3. Withdraw
The withdraw
function allows returning the deposited funds from the Hinkal smart contract to the EOA (externally owned account) of the user.
There are two ways to invoke withdraw
function: either with or without a relayer.
If a user withdraws using the relayer then his wallet address will not be posted on-chain, and the relayer's address will be revealed.
In case of no relayer, the user will submit a transaction himself.
function useWithdraw({ onError, onSuccess }: UseWithdrawParams): UseWithdrawResult;
type UseWithdrawParams = {
onError?: (err: Error) => void;
onSuccess?: () => void;
};
type UseWithdrawResult = {
isProcessing: boolean;
withdraw?: (token: ERC20Token, withdrawAmount: string, recipientAddress: string, isRelayerOff: boolean) => void;
};
4. Swap
The swap
function allows shielded swaps using Uniswap API.
The swap operation is always done using the relayer to preserve the privacy of the transaction.
As an input, the swap
function accepts in and out swap amounts and tokens in addition to Uniswap pool fee.
The pool fee is required as a parameter since for a given pair of tokens there may exist more than one pool with different fees.
function useSwap({ onError, onSuccess }: UseSwapParams): UseSwapResult;
type UseSwapParams = {
onError?: (err: Error) => void;
onSuccess?: () => void;
};
type UseSwapResult = {
isProcessing: boolean;
swap?: (
inSwapAmount: string,
inSwapToken: ERC20Token,
outSwapAmount: string,
outSwapToken: ERC20Token,
fee: number,
) => void;
};
5. Cross-chain deposit using Meson
Hinkal allows cross-chain private deposits using Meson Finance.
useMesonDeposit
hook returns mesonDeposit
function which allows creating utxo commitment in Hinkal smart contract given that a user has already called depositWithBeneficiary
function.
In addition to a token and an amount, mesonDeposit
accepts nonce which is a unique number assigned to each user transaction.
function useMesonDeposit({ onError, onSuccess }: UseMesonDepositParams): UseMesonDepositResult;
type UseMesonDepositParams = {
onError?: (err: Error) => void;
onSuccess?: () => void;
};
type UseMesonDepositResult = {
isProcessing: boolean;
mesonDeposit: (token: ERC20Token, depositAmount: string, nonce: number) => void;
};
Hinkal Context
Hinkal Context contains information about user's public shielded address, current network and SDK mode.
Wrap your main application component to context provider (HinkalContextProvider
).
<HinkalContextProvider isSandboxMode=true|false>
<your app component />
</HinkalContextProvider>
After that you'll get able to use Hinkal Context.
function useHinkalContext(): UseHinkalContextResult;
type UseHinkalContextResult = {
isSandboxMode: boolean;
selectedNetwork?: EthereumNetwork;
shieldedAddress?: string;
addEventListener: (eventType: EventType, listener: <K>(event: K) => void) => void;
removeEventListener: (eventType: EventType, listener: <K>(event: K) => void) => void;
};
type EthereumNetwork {
name: string;
chainId: number;
rpcUrl: string;
supported: boolean;
contractData?: ContractData;
};
enum EventType {
BalanceChange = 'BalanceChange',
NetworkChange = 'NetworkChange',
KycNeeded = 'KycNeeded',
}
Token List and Balances
useErc20List
hook gives the list of all supported ERC-20 tokens.
function useErc20List(): UseErc20ListResult;
type UseErc20ListResult = {
erc20List: ERC20Token[];
};
type ERC20Token = {
chainId: number;
erc20TokenAddress: string;
wrappedErc20TokenAddress?: string;
aaveTokenAddress?: string;
wrappedAaveTokenAddress?: string;
name: string;
symbol: string;
decimals: number;
logoURI?: string;
whitelisted?: boolean;
};
useHinkalBalances
hook gives the list of ERC-20 token balances.
function useHinkalBalances({ enabled }: UseHinkalBalancesParams): UseHinkalBalancesResult;
type UseHinkalBalancesParams = {
enabled?: boolean;
};
type UseHinkalBalancesResult = {
tokenBalances: TokenBalance[];
};
type TokenBalance {
token: ERC20Token;
balance: bigint;
};