@jaw.id/wagmi 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conformance.test.d.ts","sourceRoot":"","sources":["../src/conformance.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* wagmi conformance against a real @jaw.id/core.
|
|
4
|
+
*
|
|
5
|
+
* Connector.test.ts and connect-shape.test.ts replace the provider with a stub,
|
|
6
|
+
* so they pin what the connector does with an answer, not whether core still
|
|
7
|
+
* gives that answer. Here nothing in core is mocked: `JAW.create` builds the
|
|
8
|
+
* real provider, signer, store and error module in AppSpecific mode. The only
|
|
9
|
+
* stand-in is the person, a `UIHandler` that approves or rejects the way the
|
|
10
|
+
* dialogs would. A change in core that moves an event, an error code or a
|
|
11
|
+
* response shape fails here, and Nx affected runs this file on every core
|
|
12
|
+
* change because the vitest config resolves core to its source.
|
|
13
|
+
*
|
|
14
|
+
* Expected values come from the standards both sides agree on, not from running
|
|
15
|
+
* the code: EIP-1193 events and error codes (4001, 4200), EIP-3326 for an
|
|
16
|
+
* unknown chain (4902), and wagmi's connection state.
|
|
17
|
+
*/
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
19
|
+
import { Mode } from '@jaw.id/core';
|
|
20
|
+
import { getAddress, UserRejectedRequestError } from 'viem';
|
|
21
|
+
import { base, mainnet, sepolia } from 'viem/chains';
|
|
22
|
+
// Lowercase on purpose: the wallet may answer in any casing, the dapp must
|
|
23
|
+
// always see a checksummed address.
|
|
24
|
+
const ALICE = '0x00000000000000000000000000000000000a11ce';
|
|
25
|
+
const BOB = '0x0000000000000000000000000000000000000b0b';
|
|
26
|
+
/** Stands in for the person looking at the dialog. */
|
|
27
|
+
const user = { seen: [], answer: signInAs(ALICE) };
|
|
28
|
+
function signInAs(address) {
|
|
29
|
+
return (request) => ({
|
|
30
|
+
id: request.id,
|
|
31
|
+
approved: true,
|
|
32
|
+
data: { accounts: [{ address, capabilities: {} }] },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const refuse = (request) => ({ id: request.id, approved: false });
|
|
36
|
+
/**
|
|
37
|
+
* A fresh page load: new module instances, so core only knows what it
|
|
38
|
+
* rehydrates from localStorage.
|
|
39
|
+
*/
|
|
40
|
+
async function openPage(preference = {}) {
|
|
41
|
+
vi.resetModules();
|
|
42
|
+
const wagmi = await import('@wagmi/core');
|
|
43
|
+
const { jaw } = await import('./lib/Connector.js');
|
|
44
|
+
const config = wagmi.createConfig({
|
|
45
|
+
chains: [base, mainnet, sepolia],
|
|
46
|
+
connectors: [
|
|
47
|
+
jaw({
|
|
48
|
+
apiKey: 'test-api-key',
|
|
49
|
+
defaultChainId: base.id,
|
|
50
|
+
preference: {
|
|
51
|
+
...preference,
|
|
52
|
+
mode: Mode.AppSpecific,
|
|
53
|
+
uiHandler: {
|
|
54
|
+
request: async (request) => {
|
|
55
|
+
user.seen.push(request);
|
|
56
|
+
return user.answer(request);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
],
|
|
62
|
+
transports: { [base.id]: wagmi.http(), [mainnet.id]: wagmi.http(), [sepolia.id]: wagmi.http() },
|
|
63
|
+
});
|
|
64
|
+
const connector = config.connectors[0];
|
|
65
|
+
const provider = (await connector.getProvider());
|
|
66
|
+
return { wagmi, config, connector, provider };
|
|
67
|
+
}
|
|
68
|
+
async function connected(preference) {
|
|
69
|
+
const page = await openPage(preference);
|
|
70
|
+
await page.wagmi.connect(page.config, { connector: page.connector });
|
|
71
|
+
return page;
|
|
72
|
+
}
|
|
73
|
+
/** What the dapp reads from wagmi is what core holds. */
|
|
74
|
+
async function expectInSync({ wagmi, config, provider }, address, chainId) {
|
|
75
|
+
const connection = wagmi.getConnection(config);
|
|
76
|
+
expect(connection.status).toBe('connected');
|
|
77
|
+
expect(connection.address).toBe(getAddress(address));
|
|
78
|
+
expect(connection.chainId).toBe(chainId);
|
|
79
|
+
const accounts = (await provider.request({ method: 'eth_accounts' }));
|
|
80
|
+
expect(accounts.map((a) => getAddress(a))).toEqual([getAddress(address)]);
|
|
81
|
+
expect(await provider.request({ method: 'eth_chainId' })).toBe(`0x${chainId.toString(16)}`);
|
|
82
|
+
}
|
|
83
|
+
async function expectDisconnected({ wagmi, config, provider }) {
|
|
84
|
+
expect(wagmi.getConnection(config).status).toBe('disconnected');
|
|
85
|
+
expect(await provider.request({ method: 'eth_accounts' })).toEqual([]);
|
|
86
|
+
}
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
localStorage.clear();
|
|
89
|
+
user.seen = [];
|
|
90
|
+
user.answer = signInAs(ALICE);
|
|
91
|
+
// The provider builds a Communicator even in AppSpecific, and it fetches the
|
|
92
|
+
// trusted hosts list. Nothing here depends on it, and CI must stay offline.
|
|
93
|
+
vi.stubGlobal('fetch', () => Promise.reject(new Error('offline')));
|
|
94
|
+
});
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.unstubAllGlobals();
|
|
97
|
+
});
|
|
98
|
+
describe('wagmi conformance against a real core', () => {
|
|
99
|
+
describe('connect', () => {
|
|
100
|
+
it('puts wagmi and core on the same account and chain', async () => {
|
|
101
|
+
const page = await openPage();
|
|
102
|
+
const result = await page.wagmi.connect(page.config, { connector: page.connector });
|
|
103
|
+
expect(result).toEqual({ accounts: [getAddress(ALICE)], chainId: base.id });
|
|
104
|
+
await expectInSync(page, ALICE, base.id);
|
|
105
|
+
});
|
|
106
|
+
it('lands both sides on the chain the dapp asked for', async () => {
|
|
107
|
+
const page = await openPage();
|
|
108
|
+
await page.wagmi.connect(page.config, { connector: page.connector, chainId: mainnet.id });
|
|
109
|
+
await expectInSync(page, ALICE, mainnet.id);
|
|
110
|
+
});
|
|
111
|
+
it('reaches the dapp as 4001 when the user refuses, and stays disconnected', async () => {
|
|
112
|
+
user.answer = refuse;
|
|
113
|
+
const page = await openPage();
|
|
114
|
+
await expect(page.wagmi.connect(page.config, { connector: page.connector })).rejects.toMatchObject({
|
|
115
|
+
code: 4001,
|
|
116
|
+
});
|
|
117
|
+
await expectDisconnected(page);
|
|
118
|
+
});
|
|
119
|
+
// Known gap in the connector. Its catch means to rethrow a refusal as
|
|
120
|
+
// viem's UserRejectedRequestError, as wagmi's own connectors do, but it
|
|
121
|
+
// matches on the message and core says "User rejected the request", which
|
|
122
|
+
// none of its patterns cover. The dapp gets core's plain serialized error:
|
|
123
|
+
// `code` survives, `name` and `instanceof` checks do not. Turn this into a
|
|
124
|
+
// plain `it` once the connector matches on the code.
|
|
125
|
+
it.fails('reaches the dapp as the UserRejectedRequestError wagmi dapps check for', async () => {
|
|
126
|
+
user.answer = refuse;
|
|
127
|
+
const page = await openPage();
|
|
128
|
+
await expect(page.wagmi.connect(page.config, { connector: page.connector })).rejects.toBeInstanceOf(UserRejectedRequestError);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('reconnect', () => {
|
|
132
|
+
it('restores the session after a reload without asking the user', async () => {
|
|
133
|
+
await connected();
|
|
134
|
+
const asked = user.seen.length;
|
|
135
|
+
const reloaded = await openPage();
|
|
136
|
+
await reloaded.wagmi.reconnect(reloaded.config);
|
|
137
|
+
await expectInSync(reloaded, ALICE, base.id);
|
|
138
|
+
expect(user.seen).toHaveLength(asked);
|
|
139
|
+
});
|
|
140
|
+
async function reloadExpired() {
|
|
141
|
+
vi.useFakeTimers({ toFake: ['Date'] });
|
|
142
|
+
try {
|
|
143
|
+
await connected({ authTTL: 60 });
|
|
144
|
+
vi.setSystemTime(Date.now() + 61_000);
|
|
145
|
+
const reloaded = await openPage({ authTTL: 60 });
|
|
146
|
+
await reloaded.wagmi.reconnect(reloaded.config);
|
|
147
|
+
return reloaded;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
vi.useRealTimers();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
it('reloads an expired session as disconnected, still without asking', async () => {
|
|
154
|
+
const reloaded = await reloadExpired();
|
|
155
|
+
expect(reloaded.wagmi.getConnection(reloaded.config).status).toBe('disconnected');
|
|
156
|
+
// Only the sign-in from before the reload.
|
|
157
|
+
expect(user.seen).toHaveLength(1);
|
|
158
|
+
});
|
|
159
|
+
// The expiry check empties the restored signer's accounts, and the provider
|
|
160
|
+
// then drops that signer, so the next silent read is answered as a first
|
|
161
|
+
// visit ([]) rather than refused with 4100.
|
|
162
|
+
it('keeps answering eth_accounts with [] after the expiry', async () => {
|
|
163
|
+
const reloaded = await reloadExpired();
|
|
164
|
+
expect(await reloaded.provider.request({ method: 'eth_accounts' })).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('disconnect', () => {
|
|
168
|
+
it('clears both sides, and the session does not come back on reload', async () => {
|
|
169
|
+
const page = await connected();
|
|
170
|
+
await page.wagmi.disconnect(page.config);
|
|
171
|
+
await expectDisconnected(page);
|
|
172
|
+
const reloaded = await openPage();
|
|
173
|
+
await reloaded.wagmi.reconnect(reloaded.config);
|
|
174
|
+
await expectDisconnected(reloaded);
|
|
175
|
+
});
|
|
176
|
+
it('reaches wagmi when the wallet side ends the session', async () => {
|
|
177
|
+
const page = await connected();
|
|
178
|
+
await page.provider.request({ method: 'wallet_disconnect' });
|
|
179
|
+
await expectDisconnected(page);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('switchChain', () => {
|
|
183
|
+
it('moves core and wagmi together', async () => {
|
|
184
|
+
const page = await connected();
|
|
185
|
+
const chain = await page.wagmi.switchChain(page.config, { chainId: mainnet.id });
|
|
186
|
+
expect(chain.id).toBe(mainnet.id);
|
|
187
|
+
await expectInSync(page, ALICE, mainnet.id);
|
|
188
|
+
});
|
|
189
|
+
it('speaks hex chain ids on the wire, both ways', async () => {
|
|
190
|
+
const page = await connected();
|
|
191
|
+
// Pass-through spy: core still answers, the test only reads the request.
|
|
192
|
+
const request = vi.spyOn(page.provider, 'request');
|
|
193
|
+
const emitted = [];
|
|
194
|
+
page.provider.on('chainChanged', (chainId) => emitted.push(chainId));
|
|
195
|
+
await page.wagmi.switchChain(page.config, { chainId: mainnet.id });
|
|
196
|
+
// EIP-3326 takes a hex chainId; core also accepts a number, so only the
|
|
197
|
+
// request itself shows what the connector sent.
|
|
198
|
+
expect(request).toHaveBeenCalledWith({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }] });
|
|
199
|
+
// EIP-1193 chainChanged carries a hex string. The connector's Number()
|
|
200
|
+
// would hide a number here, other dapps reading the provider would not.
|
|
201
|
+
expect(emitted).toEqual(['0x1']);
|
|
202
|
+
});
|
|
203
|
+
it('refuses a chain core does not support with 4902, and nothing moves', async () => {
|
|
204
|
+
// Sepolia is configured in wagmi but is a testnet, which core only
|
|
205
|
+
// offers with preference.showTestnets.
|
|
206
|
+
const page = await connected();
|
|
207
|
+
await expect(page.wagmi.switchChain(page.config, { chainId: sepolia.id })).rejects.toMatchObject({
|
|
208
|
+
code: 4902,
|
|
209
|
+
});
|
|
210
|
+
await expectInSync(page, ALICE, base.id);
|
|
211
|
+
});
|
|
212
|
+
it('reaches wagmi when the switch goes straight to the provider', async () => {
|
|
213
|
+
const page = await connected();
|
|
214
|
+
await page.provider.request({ method: 'wallet_switchEthereumChain', params: [{ chainId: '0x1' }] });
|
|
215
|
+
await expectInSync(page, ALICE, mainnet.id);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('account change', () => {
|
|
219
|
+
it('moves wagmi to the account the user signs in with next', async () => {
|
|
220
|
+
const page = await connected();
|
|
221
|
+
user.answer = signInAs(BOB);
|
|
222
|
+
// A fresh wallet_connect with capabilities always reaches the user, the
|
|
223
|
+
// way a dapp asks for a new SIWE signature.
|
|
224
|
+
await page.provider.request({
|
|
225
|
+
method: 'wallet_connect',
|
|
226
|
+
params: [{ capabilities: { signInWithEthereum: { nonce: 'abcdef12', chainId: '0x2105' } } }],
|
|
227
|
+
});
|
|
228
|
+
await expectInSync(page, BOB, base.id);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
describe('errors on requests', () => {
|
|
232
|
+
it('a refused signature reaches the dapp as 4001', async () => {
|
|
233
|
+
const page = await connected();
|
|
234
|
+
user.answer = refuse;
|
|
235
|
+
await expect(page.wagmi.signMessage(page.config, { message: 'hello' })).rejects.toMatchObject({ code: 4001 });
|
|
236
|
+
// wagmi hex-encodes the message; the dialog must still get the text.
|
|
237
|
+
expect(user.seen.at(-1)).toMatchObject({ type: 'personal_sign', data: { message: 'hello' } });
|
|
238
|
+
await expectInSync(page, ALICE, base.id);
|
|
239
|
+
});
|
|
240
|
+
it('a method core does not offer reaches the dapp as 4200', async () => {
|
|
241
|
+
const page = await connected();
|
|
242
|
+
const client = await page.wagmi.getConnectorClient(page.config);
|
|
243
|
+
await expect(client.request({ method: 'eth_sign', params: [getAddress(ALICE), '0x00'] })).rejects.toMatchObject({ code: 4200 });
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaw.id/wagmi",
|
|
3
3
|
"description": "Wagmi connector and React hooks for JAW.id.",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.9",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -32,8 +32,30 @@
|
|
|
32
32
|
"cjs-error.cjs",
|
|
33
33
|
"!**/*.tsbuildinfo"
|
|
34
34
|
],
|
|
35
|
+
"nx": {
|
|
36
|
+
"targets": {
|
|
37
|
+
"test": {
|
|
38
|
+
"dependsOn": [
|
|
39
|
+
"^generate-sdk-info"
|
|
40
|
+
],
|
|
41
|
+
"inputs": [
|
|
42
|
+
"default",
|
|
43
|
+
"^production",
|
|
44
|
+
"{workspaceRoot}/vitest.setup.localstorage.ts",
|
|
45
|
+
{
|
|
46
|
+
"externalDependencies": [
|
|
47
|
+
"vitest"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"env": "CI"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
35
57
|
"dependencies": {
|
|
36
|
-
"@jaw.id/core": "1.5.
|
|
58
|
+
"@jaw.id/core": "1.5.5",
|
|
37
59
|
"@wagmi/core": "^3.0.0",
|
|
38
60
|
"tslib": "^2.3.0",
|
|
39
61
|
"viem": "^2.56.7"
|