Skip to content
This repository has been archived by the owner on Feb 9, 2024. It is now read-only.

Commit

Permalink
feat: add useMessageSigner and useSignatureVerification (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
DoubleOTheven authored Sep 5, 2023
1 parent f93af9a commit 4350cf1
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 12 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@polkadot/extension-dapp": "^0.46.5",
"@polkadot/extension-inject": "^0.46.5",
"@polkadot/util": "^12.3.2",
"@polkadot/util-crypto": "^12.3.2",
"@talismn/connect-wallets": "^1.2.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/useink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"React",
"hooks"
],
"version": "1.13.0",
"version": "1.14.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"description": "A React hooks library for ink! contracts",
Expand Down
3 changes: 3 additions & 0 deletions packages/useink/src/core/types/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { Signer } from '@polkadot/api/types';

export type SignatureResult = `0x${string}`;
1 change: 1 addition & 0 deletions packages/useink/src/core/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './api-contract.ts';
export * from './api.ts';
export * from './array.ts';
export * from './contracts.ts';
export * from './result.ts';
Expand Down
8 changes: 7 additions & 1 deletion packages/useink/src/core/types/talisman-connect-wallets.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export { getWalletBySource, getWallets } from '@talismn/connect-wallets';
export type { WalletAccount } from '@talismn/connect-wallets';
import { Signer } from './api';
import type { WalletAccount as TalismanWalletAccount } from '@talismn/connect-wallets';

export interface WalletAccount extends TalismanWalletAccount {
// Talisman sets the type as unknown so we must manually set it to Signer
signer?: Signer;
}
2 changes: 2 additions & 0 deletions packages/useink/src/react/hooks/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ export * from './useDeployer';
export * from './useDryRun.ts';
export * from './useEventSubscription.ts';
export * from './useEvents.ts';
export * from './useMessageSigner.ts';
export * from './useMetadata.ts';
export * from './useSalter.ts';
export * from './useSignatureVerifier.ts';
export * from './useTx.ts';
export * from './useTxEvents.ts';
export * from './useTxPaymentInfo.ts';
64 changes: 64 additions & 0 deletions packages/useink/src/react/hooks/contracts/useMessageSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { SignatureResult } from '../../../core/index.ts';
import { useWallet } from '../wallets/useWallet.ts';
import { useCallback, useState } from 'react';

export type Sign = (data?: string) => void;

export enum SignerError {
AccountNotConnected = 'No accounts are connected.',
SignatureRejected = 'Signature rejected.',
SignatureFailed = 'Signature failed.',
}

export interface MessageSigner {
sign: Sign;
signature: string | undefined;
resetState: () => void;
error: SignerError | undefined;
}

export function useMessageSigner(): MessageSigner {
const { account } = useWallet();
const [signature, setSignature] = useState<SignatureResult>();
const [error, setError] = useState<SignerError>();

const sign: Sign = useCallback(
async (data = '') => {
if (!account || !account.signer?.signRaw) {
setError(SignerError.AccountNotConnected);
return;
}

setError(undefined);

account.signer
?.signRaw?.({
address: account.address,
data,
type: 'bytes',
})
.then(({ signature }) => setSignature(signature))
.catch((e) => {
if (e.toString() === 'Error: Cancelled') {
setError(SignerError.SignatureRejected);
return;
}

setError(SignerError.SignatureFailed);
});
},
[account, account?.wallet?.extension?.signer],
);

const resetState = useCallback(() => {
setSignature(undefined);
setError(undefined);
}, []);

return {
sign,
signature,
resetState,
error,
};
}
27 changes: 22 additions & 5 deletions packages/useink/src/react/hooks/contracts/useSalter.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import { isValidHash, pseudoRandomHex } from '../../../utils';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';

export enum SalterError {
InvalidHash = 'Invalid salt hash value.',
}

export interface SalterOptions {
randomize: boolean;
initialValue: string;
length: number;
}

export interface SalterState {
salt: string;
regenerate: () => void;
set: (salt: string) => void;
error?: SalterError;
resetState: () => void;
validate: () => void;
}

export const useSalter = (): SalterState => {
const [salt, setSalt] = useState(pseudoRandomHex());
export const useSalter = (options?: Partial<SalterOptions>): SalterState => {
const { randomize = true, initialValue, length = 64 } = options || {};

const initial =
initialValue !== undefined
? initialValue
: randomize
? pseudoRandomHex(length)
: '';

const [salt, setSalt] = useState(initial);
const [error, setError] = useState<SalterError>();

useEffect(() => {
if (isValidHash(salt)) {
const validate = useCallback(() => {
if (isValidHash(salt, length)) {
error && setError(undefined);
return;
}
Expand All @@ -44,6 +60,7 @@ export const useSalter = (): SalterState => {
salt,
resetState,
regenerate,
validate,
set,
error,
};
Expand Down
50 changes: 50 additions & 0 deletions packages/useink/src/react/hooks/contracts/useSignatureVerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { signatureVerify } from '../../../utils';
import { useCallback, useState } from 'react';

type VerificationParams = Parameters<typeof signatureVerify>;

export type Verify = (
data: VerificationParams[0],
signature: VerificationParams[1],
addressOrPublicKey: VerificationParams[2],
) => void;

export enum VerificationState {
Unchecked = 'Unchecked',
Valid = 'Valid signature',
Invalid = 'Invalid signature',
}

export interface SignatureVerifier {
verify: Verify;
result: VerificationState;
resetState: () => void;
}

export function useSignatureVerifier(): SignatureVerifier {
const [result, setVerificationResult] = useState(VerificationState.Unchecked);

const verify: Verify = useCallback<Verify>(
(data, signature, addressOrPublicKey) => {
const { isValid } = signatureVerify(data, signature, addressOrPublicKey);

if (isValid) {
setVerificationResult(VerificationState.Valid);
return;
}

setVerificationResult(VerificationState.Invalid);
},
[],
);

const resetState = useCallback(() => {
setVerificationResult(VerificationState.Unchecked);
}, []);

return {
verify,
result,
resetState,
};
}
6 changes: 3 additions & 3 deletions packages/useink/src/react/providers/wallet/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const WalletProvider: React.FC<React.PropsWithChildren> = ({
}

const unsub = (await w.subscribeAccounts((accts) => {
setAccounts(accts);
setAccounts(accts as WalletAccount[]);

const firstAccount = accts?.[0];

Expand All @@ -111,7 +111,7 @@ export const WalletProvider: React.FC<React.PropsWithChildren> = ({
account && !accts?.find((a) => a.address === account?.address);

if (activeAccountNoLongerConnected) {
setWalletAccount(firstAccount);
setWalletAccount(firstAccount as WalletAccount);

if (!C.wallet?.skipAutoConnect) {
enableAutoConnect(
Expand All @@ -132,7 +132,7 @@ export const WalletProvider: React.FC<React.PropsWithChildren> = ({

const initialAccount = autoConnectAccount || firstAccount;

setWalletAccount(initialAccount);
setWalletAccount(initialAccount as WalletAccount);

if (!C.wallet?.skipAutoConnect) {
enableAutoConnect(
Expand Down
1 change: 1 addition & 0 deletions packages/useink/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from '@polkadot/util';
export { signatureVerify } from '@polkadot/util-crypto';

export * from './contracts';
export * from './events';
Expand Down
2 changes: 1 addition & 1 deletion playground/src/components/pg-deploy/DeployPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const DeployPage: React.FC = () => {
const [requireWasm, setRequireWasm] = useState(true);
const C = useCodeHash();
const M = useMetadata({ requireWasm });
const S = useSalter();
const S = useSalter({ length: 64 }); // you can omit options here bc/ the length defaults to 64 characters.
// Optionally pass in a ChainId to deploy to another chain. e.g.
// `useDeployer('shibuya-testnet')`.
// ChainId must be configured in your UseInkProvider config props.
Expand Down
86 changes: 85 additions & 1 deletion playground/src/components/pg-home/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import metadata from '../../metadata/playground.json';
import { Notifications } from '../Notifications';
import Link from 'next/link';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
/* eslint-disable @next/next/no-img-element */
import {
VerificationState,
useBalance,
useBlockHeader,
useBlockHeaders,
Expand All @@ -16,6 +17,8 @@ import {
useEventSubscription,
useEvents,
useInstalledWallets,
useMessageSigner,
useSignatureVerifier,
useTimestampDate,
useTimestampNow,
useTokenSymbol,
Expand Down Expand Up @@ -68,6 +71,11 @@ export const HomePage: React.FC = () => {
const { rpcs, setChainRpc } = useChainRpcList('astar');
const astarRpc = useChainRpc('astar');
const get = useCall<boolean>(cRococoContract, 'get');
const signer = useMessageSigner();
const signatureVerifier = useSignatureVerifier();
const [messageToSign, setMessageToSign] = useState(
'Sign this message, or change me and then sign!',
);
const getSubcription = useCallSubscription<boolean>(
cRococoContract,
'get',
Expand Down Expand Up @@ -615,6 +623,82 @@ export const HomePage: React.FC = () => {
</h3>
</li>

<li>
<h3 className='text-xl'>Sign a message, and Verify it</h3>
<input
className='w-full p-3 mt-3 rounded-md text-brand-800 font-semibold'
value={messageToSign}
onChange={(e) => setMessageToSign(e.target.value)}
/>
<button
type='button'
onClick={() => {
signatureVerifier.result !== VerificationState.Unchecked &&
signatureVerifier.resetState();

signer.sign(messageToSign);
}}
className='mt-3 w-full rounded-2xl text-white px-6 py-4 bg-blue-500 hover:bg-blue-600 transition duration-75'
>
Sign Message
</button>

<button
type='button'
onClick={() => {
setMessageToSign('');
signer.resetState();
}}
className='mt-3 w-full rounded-2xl text-white px-6 py-4 bg-blue-500 hover:bg-blue-600 transition duration-75'
>
Reset State
</button>

{signer.signature && account?.address && (
<div className='mt-3'>
<textarea
className='text-sm text-black min-h-[80px] w-full rounded-md p-3'
value={signer.signature}
/>

<button
type='button'
onClick={() => {
signatureVerifier.verify(
messageToSign,
signer.signature || '',
account?.address,
);
}}
className='mt-3 w-full rounded-2xl text-white px-6 py-4 bg-blue-500 hover:bg-blue-600 transition duration-75'
>
Verify Signature
</button>

<button
type='button'
disabled={
signatureVerifier.result === VerificationState.Unchecked
}
onClick={() => {
signatureVerifier.resetState();
}}
className='mt-3 w-full disabled:bg-blue-50/50 rounded-2xl text-white px-6 py-4 bg-blue-500 hover:bg-blue-600 transition duration-75'
>
Reset Verification State
</button>

<p className='text-sm mt-3'>
Verification Status: {signatureVerifier.result}
</p>
</div>
)}

{signer.error && (
<p className='text-sm text-error-500 mt-3'>{signer.error}</p>
)}
</li>

<li>
<h3 className='text-xl'>Deploy a Contract</h3>
<Link
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4350cf1

Please sign in to comment.