Skip to content

Commit c866944

Browse files
authored
feat!: add watch into parameters of useContractQuery (#60)
Closes #61 Changes: - Add `watch` into the parameters of useContractQuery. - Remove useWatchContractQuery.
2 parents ff71016 + fc6d813 commit c866944

File tree

7 files changed

+193
-279
lines changed

7 files changed

+193
-279
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ const { connectedAccount, signer } = ... // from subconnect or talisman-connect
191191
- `useContract`: Create & manage `Contract` instance given its unique id from the registered contract deployments
192192
- `useContractTx`: Provides functionality to sign and send transactions to a smart contract, and tracks the progress of the transaction.
193193
- `useContractQuery`: Help making a contract query
194-
- `useWatchContractQuery`: Similar to `useContractQuery` with ability to watch for changes
195194
- `useDeployer`: Create & manage `ContractDeployer` instance given its unique id from the registered contract deployments
196195
- `useDeployerTx`: Similar to `useContractTx`, this hook provides functionality to sign and send transactions to deploy a smart contract, and tracks the progress of the transaction.
197196
- `useWatchContractEvent`: Help watch for a specific contract event and perform a specific action

e2e/zombienet/src/hooks/useContractQuery.test.ts

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
22
import { ALICE, BOB, CHARLIE, deployPsp22Contract, psp22Metadata, wrapper } from '../utils';
33
import { numberToHex } from 'dedot/utils';
44
import { renderHook, waitFor } from '@testing-library/react';
5-
import { useContractQuery } from 'typink';
5+
import { useContractQuery, useContractTx, useRawContract } from 'typink';
66
import { Contract } from 'dedot/contracts';
77
import { Psp22ContractApi } from 'contracts/psp22';
88

@@ -15,15 +15,18 @@ describe('useContractQuery', () => {
1515
contract = new Contract<Psp22ContractApi>(client, psp22Metadata, contractAddress, { defaultCaller: ALICE });
1616
});
1717

18-
it('should load total supply', async () => {
19-
const { result } = renderHook(() => useContractQuery({ contract, fn: 'psp22TotalSupply' }), {
18+
it('should work probably', async () => {
19+
const { result: totalSupply } = renderHook(() => useContractQuery({ contract, fn: 'psp22TotalSupply' }), {
2020
wrapper,
2121
});
2222

23-
expect(result.current.data).toBeUndefined();
23+
expect(totalSupply.current.data).toBeUndefined();
24+
expect(totalSupply.current.isLoading).toEqual(true);
25+
expect(totalSupply.current.isRefreshing).toEqual(false);
2426

2527
await waitFor(() => {
26-
expect(result.current.data).toBe(BigInt(1e20));
28+
expect(totalSupply.current.data).toEqual(BigInt(1e20));
29+
expect(totalSupply.current.isLoading).toEqual(false);
2730
});
2831
});
2932

@@ -39,6 +42,62 @@ describe('useContractQuery', () => {
3942
});
4043
});
4144

45+
it('should automatically call refresh on new block when watch is enabled', async () => {
46+
const { result: rawContract } = renderHook(() => useRawContract<Psp22ContractApi>(psp22Metadata, contractAddress), {
47+
wrapper,
48+
});
49+
50+
await waitFor(() => {
51+
expect(rawContract.current.contract).toBeDefined();
52+
expect(rawContract.current.contract?.client.options.signer).toBeDefined();
53+
});
54+
55+
const contract = rawContract.current.contract;
56+
57+
const { result: balanceOf } = renderHook(
58+
() => useContractQuery({ contract, fn: 'psp22BalanceOf', args: [ALICE], watch: true }),
59+
{
60+
wrapper,
61+
},
62+
);
63+
64+
await waitFor(() => {
65+
expect(balanceOf.current.data).toBeDefined();
66+
});
67+
68+
const beforeTranfer = balanceOf.current.data!;
69+
console.log('Before transfer:', beforeTranfer);
70+
71+
const { result: transfer } = renderHook(
72+
() => useContractTx(contract, 'psp22Transfer'), // prettier-end-here
73+
{
74+
wrapper,
75+
},
76+
);
77+
78+
await new Promise<void>((resolve) => {
79+
transfer.current.signAndSend({
80+
args: [BOB, BigInt(1e12), '0x'],
81+
callback: ({ status }) => {
82+
if (status.type === 'Finalized') {
83+
resolve();
84+
}
85+
},
86+
});
87+
});
88+
console.log('Transfer completed!');
89+
90+
await waitFor(
91+
() => {
92+
expect(balanceOf.current.data).toBeDefined();
93+
expect(balanceOf.current.data).toEqual(beforeTranfer - BigInt(1e12));
94+
},
95+
{ timeout: 12000 },
96+
);
97+
98+
console.log('After transfer:', balanceOf.current.data);
99+
});
100+
42101
it('should fail dry-run', async () => {
43102
const { result } = renderHook(
44103
() =>
@@ -61,4 +120,17 @@ describe('useContractQuery', () => {
61120
expect(result.current.data?.err).toEqual({ type: 'InsufficientBalance' });
62121
});
63122
});
123+
124+
it('should define error when errors occured', async () => {
125+
const { result: balanceOf } = renderHook(
126+
() => useContractQuery({ contract, args: ['0x__FAKE'], fn: 'psp22BalanceOf' }),
127+
{ wrapper },
128+
);
129+
130+
await waitFor(() => {
131+
expect(balanceOf.current.error).toBeDefined();
132+
});
133+
134+
console.log('Error:', balanceOf.current.error);
135+
});
64136
});

examples/demo-subconnect/src/components/GreeterBoard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import PendingText from '@/components/shared/PendingText.tsx';
55
import { shortenAddress } from '@/utils/string.ts';
66
import { ContractId } from 'contracts/deployments';
77
import { GreeterContractApi } from 'contracts/types/greeter';
8-
import { useContract, useContractTx, useWatchContractEvent, useWatchContractQuery } from 'typink';
8+
import { useContract, useContractQuery, useContractTx, useWatchContractEvent } from 'typink';
99
import { txToaster } from '@/utils/txToaster.tsx';
1010

1111
export default function GreetBoard() {
1212
const { contract } = useContract<GreeterContractApi>(ContractId.GREETER);
1313
const [message, setMessage] = useState('');
1414
const setMessageTx = useContractTx(contract, 'setMessage');
1515

16-
const { data: greet, isLoading } = useWatchContractQuery({
16+
const { data: greet, isLoading } = useContractQuery({
1717
contract,
1818
fn: 'greet',
19+
watch: true,
1920
});
2021

2122
const handleUpdateGreeting = async () => {

examples/demo/src/components/GreeterBoard.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import PendingText from '@/components/shared/PendingText.tsx';
55
import { shortenAddress } from '@/utils/string.ts';
66
import { ContractId } from 'contracts/deployments';
77
import { GreeterContractApi } from 'contracts/types/greeter';
8-
import { useContract, useContractTx, useWatchContractEvent, useWatchContractQuery } from 'typink';
8+
import { useContract, useContractQuery, useContractTx, useWatchContractEvent } from 'typink';
99
import { txToaster } from '@/utils/txToaster.tsx';
1010

1111
export default function GreetBoard() {
1212
const { contract } = useContract<GreeterContractApi>(ContractId.GREETER);
1313
const [message, setMessage] = useState('');
1414
const setMessageTx = useContractTx(contract, 'setMessage');
1515

16-
const { data: greet, isLoading } = useWatchContractQuery({
16+
const { data: greet, isLoading } = useContractQuery({
1717
contract,
1818
fn: 'greet',
19+
watch: true,
1920
});
2021

2122
const handleUpdateGreeting = async () => {

packages/typink/src/hooks/__tests__/useContractQuery.test.ts

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { act, renderHook } from '@testing-library/react';
1+
import { act, renderHook, waitFor } from '@testing-library/react';
22
import { useContractQuery } from '../useContractQuery.js';
33
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
44
import { Contract } from 'dedot/contracts';
55
import { waitForNextUpdate } from './test-utils.js';
6+
import { useTypink } from '../useTypink.js';
67

78
// Mock the external dependencies
89
vi.mock('react-use', () => ({
@@ -14,15 +15,32 @@ vi.mock('./internal/index.js', () => ({
1415
useRefresher: vi.fn(() => ({ refresh: vi.fn(), counter: 0 })),
1516
}));
1617

18+
vi.mock('../useTypink', () => ({
19+
useTypink: vi.fn(),
20+
}));
21+
1722
describe('useContractQuery', () => {
1823
let contract: Contract<any>;
1924

25+
// Mock the client
26+
const mockClient = {
27+
query: {
28+
system: {
29+
number: vi.fn().mockResolvedValue(0),
30+
},
31+
},
32+
};
33+
2034
beforeEach(() => {
2135
contract = {
2236
query: {
37+
message: vi.fn().mockResolvedValue({ data: 'initial result' }),
2338
testFunction: vi.fn(),
2439
},
40+
client: mockClient,
2541
} as any;
42+
43+
vi.mocked(useTypink).mockReturnValue({ client: mockClient } as any);
2644
});
2745

2846
afterEach(() => {
@@ -117,9 +135,14 @@ describe('useContractQuery', () => {
117135
expect(contract.query.testFunction).toHaveBeenCalledTimes(3);
118136
});
119137

120-
it('should handle errors from the contract query', async () => {
121-
const testError = new Error('Test error');
122-
contract.query.testFunction.mockRejectedValue(testError);
138+
it('should update refresh state when refresh function is called', async () => {
139+
contract.query.testFunction.mockImplementation(() => {
140+
return new Promise((resolve) => {
141+
setTimeout(() => {
142+
resolve({ data: 'test result' });
143+
}, 100);
144+
});
145+
});
123146

124147
const { result } = renderHook(() =>
125148
useContractQuery({
@@ -129,14 +152,39 @@ describe('useContractQuery', () => {
129152
}),
130153
);
131154

155+
expect(result.current.isRefreshing).toBe(false);
156+
132157
await act(async () => {
133-
await new Promise((resolve) => setTimeout(resolve, 0));
158+
result.current.refresh();
134159
});
135160

136-
expect(result.current.isLoading).toBe(false);
137-
expect(result.current.error).toBe(testError);
138-
expect(result.current.data).toBeUndefined();
139-
});
161+
expect(result.current.isRefreshing).toBe(true);
162+
163+
await waitFor(() => {
164+
expect(result.current.isRefreshing).toBe(false);
165+
expect(result.current.data).toBe('test result');
166+
});
167+
}),
168+
it('should handle errors from the contract query', async () => {
169+
const testError = new Error('Test error');
170+
contract.query.testFunction.mockRejectedValue(testError);
171+
172+
const { result } = renderHook(() =>
173+
useContractQuery({
174+
contract,
175+
// @ts-ignore
176+
fn: 'testFunction',
177+
}),
178+
);
179+
180+
await act(async () => {
181+
await new Promise((resolve) => setTimeout(resolve, 0));
182+
});
183+
184+
expect(result.current.isLoading).toBe(false);
185+
expect(result.current.error).toBe(testError);
186+
expect(result.current.data).toBeUndefined();
187+
});
140188

141189
it('should reset error state on successful query after an error', async () => {
142190
const testError = new Error('Test error');
@@ -182,4 +230,40 @@ describe('useContractQuery', () => {
182230
expect(result.current.error).toBeUndefined();
183231
expect(result.current.data).toBeUndefined();
184232
});
233+
234+
it('should refresh when client.query.system.number changes when watch is enabled', async () => {
235+
// Simulate a block number change
236+
mockClient.query.system.number.mockImplementation((callback) => {
237+
return new Promise((resolve) => {
238+
setTimeout(() => {
239+
callback(2);
240+
setTimeout(() => {
241+
callback(3);
242+
}, 50);
243+
}, 100);
244+
245+
resolve(() => {});
246+
});
247+
});
248+
249+
const { result } = renderHook(() =>
250+
useContractQuery({
251+
contract,
252+
// @ts-ignore
253+
fn: 'message',
254+
watch: true,
255+
}),
256+
);
257+
258+
await waitForNextUpdate();
259+
260+
expect(result.current.data).toEqual('initial result');
261+
262+
contract.query.message.mockResolvedValue({ data: 'updated result' });
263+
264+
await waitFor(() => {
265+
expect(contract.query.message).toHaveBeenCalledTimes(3);
266+
expect(result.current.data).toEqual('updated result');
267+
});
268+
});
185269
});

0 commit comments

Comments
 (0)