Skip to content

Commit

Permalink
feat: share signer in context + improve useAttest
Browse files Browse the repository at this point in the history
  • Loading branch information
izziaraffaele committed Aug 13, 2023
1 parent bd06290 commit 25c2844
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 54 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"react-dom": ">=17"
},
"dependencies": {
"@ethereum-attestation-service/eas-sdk": "^1.0.0-beta.0"
"@ethereum-attestation-service/eas-sdk": "^1.1.0-beta.1"
},
"devDependencies": {
"@testing-library/react": "^14.0.0",
Expand Down
4 changes: 3 additions & 1 deletion src/EasProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { EAS, EASOptions } from '@ethereum-attestation-service/eas-sdk';
// hooks
import { useEasController } from './hooks/useEasController';

export const EasContext = createContext<EAS | null>(null);
export type EasContextValue = ReturnType<typeof useEasController>;

export type EasProviderProps = {
eas?: EAS;
address?: string;
options?: EASOptions;
};

export const EasContext = createContext<EasContextValue | null>(null);

export function EasProvider({
eas,
address,
Expand Down
14 changes: 8 additions & 6 deletions src/hooks/useAttest.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseWallet, Signer } from 'ethers';
import { Signer } from 'ethers';
import {
AttestationRequestData,
OffchainAttestationParams,
Expand All @@ -12,7 +12,9 @@ export type AttestationData = {
};

export function useAttest(signer?: Signer) {
const eas = useEasContext();
const { eas, signer: defaultSigner } = useEasContext();

const attestationSigner = signer || defaultSigner;

return useMemo(
() => ({
Expand All @@ -27,16 +29,16 @@ export function useAttest(signer?: Signer) {
offchain: async (schema: string, data: AttestationData['offchain']) => {
const offchain = await eas.getOffchain();

if (!signer) {
throw new Error('invalid signer');
if (!attestationSigner) {
throw new Error('Signing offchain attestations requires a signer.');
}

return offchain.signOffchainAttestation(
{ ...data, schema },
signer as unknown as BaseWallet
attestationSigner
);
},
}),
[signer, eas]
[attestationSigner, eas]
);
}
21 changes: 19 additions & 2 deletions src/hooks/useEasController.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useMemo } from 'react';
// eas
import { EAS, EASOptions } from '@ethereum-attestation-service/eas-sdk';
import { Signer } from 'ethers';

// EAS mainnet contract
const DEFAULT_CONTRACT_ADDRESS = '0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587';

export function useEasController(
address: string | EAS = DEFAULT_CONTRACT_ADDRESS,
options?: EASOptions
options: EASOptions = {}
) {
const eas = useMemo(() => {
if (address instanceof EAS) {
Expand All @@ -17,5 +18,21 @@ export function useEasController(
return new EAS(address, options);
}, [address, options]);

return eas;
const { contract } = eas;

const signer = useMemo(() => {
return isSigner(contract.runner) ? contract.runner : null;
}, [contract]);

return { eas, signer };
}

function isSigner(obj: unknown): obj is Signer {
return (
typeof obj === 'object' &&
obj !== null &&
'provider' in obj &&
'signMessage' in obj &&
'signTypedData' in obj
);
}
70 changes: 56 additions & 14 deletions test/scenario/hooks/useAttest.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Signer } from 'ethers';
import { Signer, toBigInt } from 'ethers';

import { renderHook, initEnvironment } from '../../test-environment';
import { useAttest, AttestationData } from '../../../src/hooks/useAttest'; // Adjusted path

const { sender } = initEnvironment();
const { sender, receiver: sender2 } = initEnvironment();

const mockOffchain = {
signOffchainAttestation: jest.fn().mockResolvedValue('mockSignedAttestation'),
Expand All @@ -17,18 +17,24 @@ const easMock = {
getOffchain: () => Promise.resolve(mockOffchain),
};

const easContextMock = {
eas: easMock,
signer: sender as Signer | null,
};

jest.mock('../../../src/hooks/useEasContext', () => ({
useEasContext: jest.fn(() => easMock),
useEasContext: jest.fn(() => easContextMock),
}));

describe('useAttest()', () => {
let signer: Signer | undefined = sender;
let signer: Signer | undefined;

const renderTest = () => renderHook(() => useAttest(signer));

beforeEach(() => {
jest.clearAllMocks();
signer = sender;
signer = undefined;
easContextMock.signer = sender;
});

it('provides onchain and offchain functions', () => {
Expand All @@ -42,37 +48,73 @@ describe('useAttest()', () => {
it('works as expected', async () => {
const { result } = renderTest();

const attestationData: AttestationData['onchain'] = {
recipient: '0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165',
expirationTime: toBigInt(0),
revocable: true, // Be aware that if your schema is not revocable, this MUST be false
data: 'encodedData',
};

await expect(
result.current.onchain('mockSchema', {} as AttestationData['onchain'])
result.current.onchain('mockSchema', attestationData)
).resolves.toBe('mockAttestation');
expect(easMock.attest).toHaveBeenCalledWith({
schema: 'mockSchema',
data: {},
data: attestationData,
});
expect(easMock.getAttestation).toHaveBeenCalledWith('mockAttestationUID');
});
});

describe('offchain', () => {
it('offchain function throws error when no signer is provided', async () => {
signer = undefined;
const attestationData: AttestationData['offchain'] = {
recipient: '0xFD50b031E778fAb33DfD2Fc3Ca66a1EeF0652165',
expirationTime: toBigInt(0),
time: toBigInt(1671219636),
revocable: true,
version: 1,
nonce: toBigInt(0),
refUID:
'0x0000000000000000000000000000000000000000000000000000000000000000',
data: 'encodedData',
};

it('throws error when no signer is provided', async () => {
easContextMock.signer = null;
const { result } = renderTest();

await expect(
result.current.offchain('mockSchema', attestationData)
).rejects.toThrow('Signing offchain attestations requires a signer.');
});

it('works as expected with default signer', async () => {
const { result } = renderTest();

await expect(
result.current.offchain('mockSchema', {} as AttestationData['offchain'])
).rejects.toThrow('invalid signer');
result.current.offchain('mockSchema', attestationData)
).resolves.toBe('mockSignedAttestation');

expect(mockOffchain.signOffchainAttestation).toHaveBeenCalledWith(
{
...attestationData,
schema: 'mockSchema',
},
easContextMock.signer
);
});

it('offchain function works as expected with valid signer', async () => {
it('works as expected with custom signer', async () => {
signer = sender2;
const { result } = renderTest();

await expect(
result.current.offchain('mockSchema', {} as AttestationData['offchain'])
result.current.offchain('mockSchema', attestationData)
).resolves.toBe('mockSignedAttestation');

expect(mockOffchain.signOffchainAttestation).toHaveBeenCalledWith(
{
...{},
...attestationData,
schema: 'mockSchema',
},
signer
Expand Down
55 changes: 35 additions & 20 deletions test/scenario/hooks/useEasController.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,52 +29,67 @@ describe('useEasController()', () => {
});

it('should return the default EAS instance when no parameters are provided', async () => {
const { result } = renderTest();

expect(result.current).toBeInstanceOf(EAS);
await expect(result.current.contract.getAddress()).resolves.toBe(
MAINNET_ADDRESS
);
const {
result: {
current: { eas },
},
} = renderTest();

expect(eas).toBeInstanceOf(EAS);
await expect(eas.contract.getAddress()).resolves.toBe(MAINNET_ADDRESS);
});

it('should return the passed EAS instance when an instance of EAS is provided', () => {
inputs.eas = new EAS(MAINNET_ADDRESS);

const { result } = renderTest();
const {
result: {
current: { eas },
},
} = renderTest();

expect(result.current).toBe(inputs.eas);
expect(eas).toBe(inputs.eas);
});

it('should return a new EAS instance with the provided address string', async () => {
inputs.address = SEPOLIA_ADDRESS;

const { result } = renderTest();
const {
result: {
current: { eas },
},
} = renderTest();

expect(result.current).toBeInstanceOf(EAS);
await expect(result.current.contract.getAddress()).resolves.toBe(
inputs.address
);
expect(eas).toBeInstanceOf(EAS);
await expect(eas.contract.getAddress()).resolves.toBe(inputs.address);
});

it('should respect the options parameter when constructing a new EAS instance', () => {
inputs.options = {
signerOrProvider: sender,
};

const { result: resultWithOpts } = renderTest();
const {
result: {
current: { eas },
},
} = renderTest();

expect(resultWithOpts.current.contract.runner).toBe(
inputs.options.signerOrProvider
);
expect(eas.contract.runner).toBe(inputs.options.signerOrProvider);
});

it('should not recreate the EAS instance unnecessarily due to memoization', () => {
const { result, rerender } = renderTest();
const {
result: {
current: { eas },
},
rerender,
} = renderTest();

const initialInstance = result;
const initialInstance = eas;

rerender(); // Re-render the hook

expect(result).toBe(initialInstance);
expect(eas).toBe(initialInstance);
});
});
5 changes: 1 addition & 4 deletions test/scenario/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
SchemaEncoder,
SchemaItem,
} from '@ethereum-attestation-service/eas-sdk';
import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk';
import { encodeAttestationData } from '../../src/utils';

jest.mock('@ethereum-attestation-service/eas-sdk', () => ({
Expand Down
1 change: 1 addition & 0 deletions test/test-environment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function initEnvironment() {
const sender = new Wallet(
'0x0123456789012345678901234567890123456789012345678901234567890123'
);

const receiver = new Wallet(
'0x6789012345678901230123456789012345678901234567890123456789012345'
);
Expand Down
12 changes: 6 additions & 6 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -499,13 +499,13 @@
dependencies:
hardhat "2.17.0"

"@ethereum-attestation-service/eas-sdk@^1.0.0-beta.0":
version "1.0.0-beta.0"
resolved "https://registry.yarnpkg.com/@ethereum-attestation-service/eas-sdk/-/eas-sdk-1.0.0-beta.0.tgz#c30186af8b7c6f31a6b0e7438208bb757b91126b"
integrity sha512-sQ7e8mu1Q+6kZe+Mva/6jOvwiiyrYdnLNkHX6xZayQB2YswUPnMHIDDeTRECcmnyUbv/DxE6NDCZM42yUYhqcw==
"@ethereum-attestation-service/eas-sdk@^1.1.0-beta.1":
version "1.1.0-beta.1"
resolved "https://registry.yarnpkg.com/@ethereum-attestation-service/eas-sdk/-/eas-sdk-1.1.0-beta.1.tgz#63c1dd7877e5427a9866248a2157f04915af68a4"
integrity sha512-LJT4SZY4MsYeT34wf8+7A8adLFF24Pos0QW+e66eMpNL6+Eqts9NHodXE1duts5PD6MJUKVWsSYAOxZZgZ+S4Q==
dependencies:
"@ethereum-attestation-service/eas-contracts" "1.0.0-beta.0"
ethers "^6.6.5"
ethers "^6.7.0"
js-base64 "^3.7.5"
multiformats "9.9.0"
pako "^2.1.0"
Expand Down Expand Up @@ -3600,7 +3600,7 @@ ethers@^5.7.1:
"@ethersproject/web" "5.7.1"
"@ethersproject/wordlists" "5.7.0"

ethers@^6.6.5, ethers@^6.7.0:
ethers@^6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.7.0.tgz#0f772c31a9450de28aa518b181c8cb269bbe7fd1"
integrity sha512-pxt5hK82RNwcTX2gOZP81t6qVPVspnkpeivwEgQuK9XUvbNtghBnT8GNIb/gPh+WnVSfi8cXC9XlfT8sqc6D6w==
Expand Down

0 comments on commit 25c2844

Please sign in to comment.