@arcanahq/sdk-integrations 0.1.0

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.
package/dist/test.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { ArcanaWalletAdapter } from '@arcanahq/sdk';
2
+ export declare const DEFAULT_ARCANA_TEST_PRIVATE_KEY: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
3
+ export interface TestArcanaAdapterOptions {
4
+ /**
5
+ * Deterministic private key used only for local/test auth. Defaults to the
6
+ * first Anvil account so local agents can authenticate without OTP flows.
7
+ */
8
+ privateKey?: `0x${string}`;
9
+ /** Chain id to report to Arcana device registration. */
10
+ chainId?: number;
11
+ }
12
+ export declare function createTestArcanaWalletAdapter(options?: TestArcanaAdapterOptions): ArcanaWalletAdapter;
package/dist/test.js ADDED
@@ -0,0 +1,25 @@
1
+ import { privateKeyToAccount } from 'viem/accounts';
2
+ import { assertHexAddress } from './common.js';
3
+ export const DEFAULT_ARCANA_TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
4
+ export function createTestArcanaWalletAdapter(options = {}) {
5
+ const privateKey = options.privateKey ?? DEFAULT_ARCANA_TEST_PRIVATE_KEY;
6
+ const account = privateKeyToAccount(privateKey);
7
+ const address = assertHexAddress(account.address, 'test wallet address');
8
+ const chainId = options.chainId ?? 31337;
9
+ return {
10
+ getAddress() {
11
+ return address;
12
+ },
13
+ getChainId() {
14
+ return chainId;
15
+ },
16
+ async signTypedData(request) {
17
+ return account.signTypedData({
18
+ domain: request.domain,
19
+ types: request.types,
20
+ primaryType: request.primaryType,
21
+ message: request.message,
22
+ });
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,32 @@
1
+ import type { ArcanaWalletAdapter } from '@arcanahq/sdk';
2
+ import { AdapterOptions, ViemWalletClientLike } from './common.js';
3
+ export { registerArcanaDeviceWithAdapter } from './authenticate.js';
4
+ export type { ArcanaClientForDeviceRegistration, RegisterArcanaDeviceOptions } from './authenticate.js';
5
+ export interface ZeroDevAccountLike {
6
+ address?: string;
7
+ }
8
+ export interface ZeroDevClientLike extends ViemWalletClientLike {
9
+ account?: ZeroDevAccountLike | `0x${string}` | null;
10
+ }
11
+ export interface ZeroDevArcanaAdapterOptions extends AdapterOptions {
12
+ /**
13
+ * EOA or embedded wallet signer that controls the Kernel account.
14
+ *
15
+ * Arcana device registration currently verifies ECDSA recovery against
16
+ * user_address, so the signer must be the controlling wallet, not the Kernel
17
+ * smart-account address.
18
+ */
19
+ ownerWalletClient?: ViemWalletClientLike;
20
+ /** Alias used by Dynamic's ZeroDev helper docs. */
21
+ signerWalletClient?: ViemWalletClientLike;
22
+ /** Optional smart account address for app bookkeeping. It is not used for Arcana auth. */
23
+ smartAccountAddress?: string;
24
+ /** @deprecated Pass ownerWalletClient/signerWalletClient. Kernel signatures are not valid for Arcana auth yet. */
25
+ kernelClient?: ZeroDevClientLike;
26
+ /** @deprecated Use smartAccountAddress. */
27
+ smartAccount?: ZeroDevAccountLike;
28
+ }
29
+ export interface ZeroDevArcanaWalletAdapter extends ArcanaWalletAdapter {
30
+ getSmartAccountAddress(): `0x${string}` | null;
31
+ }
32
+ export declare function createZeroDevArcanaWalletAdapter(options: ZeroDevArcanaAdapterOptions): ZeroDevArcanaWalletAdapter;
@@ -0,0 +1,27 @@
1
+ import { assertHexAddress, createViemArcanaWalletAdapter, } from './common.js';
2
+ export { registerArcanaDeviceWithAdapter } from './authenticate.js';
3
+ export function createZeroDevArcanaWalletAdapter(options) {
4
+ const ownerWalletClient = options.ownerWalletClient ?? options.signerWalletClient;
5
+ if (!ownerWalletClient) {
6
+ throw new Error('ZeroDev Arcana integration requires ownerWalletClient/signerWalletClient for Arcana auth; Kernel smart-account signatures are not accepted by Arcana device registration yet');
7
+ }
8
+ if (options.kernelClient && !options.ownerWalletClient && !options.signerWalletClient) {
9
+ throw new Error('ZeroDev kernelClient was provided without an owner signer. Pass ownerWalletClient/signerWalletClient for Arcana auth');
10
+ }
11
+ const smartAccountAddress = options.smartAccountAddress
12
+ ?? (typeof options.kernelClient?.account === 'string' ? options.kernelClient.account : options.kernelClient?.account?.address)
13
+ ?? options.smartAccount?.address
14
+ ?? null;
15
+ const adapter = createViemArcanaWalletAdapter(ownerWalletClient, {
16
+ ...options,
17
+ address: options.address,
18
+ });
19
+ return {
20
+ ...adapter,
21
+ getSmartAccountAddress() {
22
+ return smartAccountAddress
23
+ ? assertHexAddress(smartAccountAddress, 'ZeroDev smart account address')
24
+ : null;
25
+ },
26
+ };
27
+ }
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@arcanahq/sdk-integrations",
3
+ "version": "0.1.0",
4
+ "description": "Optional wallet and account-abstraction integrations for @arcanahq/sdk",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
23
+ "./privy": {
24
+ "import": "./dist/privy.js",
25
+ "types": "./dist/privy.d.ts",
26
+ "default": "./dist/privy.js"
27
+ },
28
+ "./dynamic": {
29
+ "import": "./dist/dynamic.js",
30
+ "types": "./dist/dynamic.d.ts",
31
+ "default": "./dist/dynamic.js"
32
+ },
33
+ "./zerodev": {
34
+ "import": "./dist/zerodev.js",
35
+ "types": "./dist/zerodev.d.ts",
36
+ "default": "./dist/zerodev.js"
37
+ },
38
+ "./authenticate": {
39
+ "import": "./dist/authenticate.js",
40
+ "types": "./dist/authenticate.d.ts",
41
+ "default": "./dist/authenticate.js"
42
+ },
43
+ "./test": {
44
+ "import": "./dist/test.js",
45
+ "types": "./dist/test.d.ts",
46
+ "default": "./dist/test.js"
47
+ },
48
+ "./privy-zerodev": {
49
+ "import": "./dist/privy-zerodev.js",
50
+ "types": "./dist/privy-zerodev.d.ts",
51
+ "default": "./dist/privy-zerodev.js"
52
+ },
53
+ "./dynamic-zerodev": {
54
+ "import": "./dist/dynamic-zerodev.js",
55
+ "types": "./dist/dynamic-zerodev.d.ts",
56
+ "default": "./dist/dynamic-zerodev.js"
57
+ }
58
+ },
59
+ "scripts": {
60
+ "build": "tsc",
61
+ "dev": "tsc --watch",
62
+ "test": "vitest run",
63
+ "prepublishOnly": "npm run build"
64
+ },
65
+ "peerDependencies": {
66
+ "@arcanahq/sdk": "^0.1.0",
67
+ "@dynamic-labs-sdk/client": "*",
68
+ "@dynamic-labs-sdk/evm": "*",
69
+ "@dynamic-labs-sdk/react-hooks": "*",
70
+ "@dynamic-labs-sdk/zerodev": "*",
71
+ "@dynamic-labs/sdk-react-core": "*",
72
+ "@dynamic-labs/viem-utils": "*",
73
+ "@privy-io/react-auth": "*",
74
+ "@zerodev/sdk": "*",
75
+ "viem": "^2.0.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@dynamic-labs-sdk/client": {
79
+ "optional": true
80
+ },
81
+ "@dynamic-labs-sdk/evm": {
82
+ "optional": true
83
+ },
84
+ "@dynamic-labs-sdk/react-hooks": {
85
+ "optional": true
86
+ },
87
+ "@dynamic-labs-sdk/zerodev": {
88
+ "optional": true
89
+ },
90
+ "@dynamic-labs/sdk-react-core": {
91
+ "optional": true
92
+ },
93
+ "@dynamic-labs/viem-utils": {
94
+ "optional": true
95
+ },
96
+ "@privy-io/react-auth": {
97
+ "optional": true
98
+ },
99
+ "@zerodev/sdk": {
100
+ "optional": true
101
+ },
102
+ "viem": {
103
+ "optional": true
104
+ }
105
+ },
106
+ "devDependencies": {
107
+ "@arcanahq/sdk": "file:../sdk",
108
+ "@types/node": "^20.19.41",
109
+ "typescript": "^5.3.0",
110
+ "viem": "^2.0.0",
111
+ "vitest": "^4.1.8"
112
+ }
113
+ }
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ createEip1193ArcanaWalletAdapter,
4
+ createViemArcanaWalletAdapter,
5
+ } from '../common.js';
6
+ import {
7
+ authenticateArcanaWithDynamic,
8
+ createDynamicArcanaWalletAdapter,
9
+ ensureArcanaSessionWithDynamic,
10
+ } from '../dynamic.js';
11
+ import { createZeroDevArcanaWalletAdapter } from '../zerodev.js';
12
+ import { registerArcanaDeviceWithAdapter } from '../authenticate.js';
13
+ import { createTestArcanaWalletAdapter } from '../test.js';
14
+
15
+ const address = '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' as const;
16
+ const verifyingContract = '0x1212121212121212121212121212121212121212' as const;
17
+
18
+ const typedData = {
19
+ account: address,
20
+ domain: {
21
+ name: 'Arcana',
22
+ version: '1',
23
+ chainId: 31337,
24
+ verifyingContract,
25
+ },
26
+ types: {
27
+ DeviceRegistration: [
28
+ { name: 'userAddress', type: 'address' },
29
+ { name: 'devicePubkey', type: 'string' },
30
+ { name: 'timestamp', type: 'uint256' },
31
+ { name: 'nonce', type: 'string' },
32
+ ],
33
+ },
34
+ primaryType: 'DeviceRegistration',
35
+ message: {
36
+ userAddress: address,
37
+ devicePubkey: 'ab'.repeat(32),
38
+ timestamp: 1n,
39
+ nonce: 'nonce',
40
+ },
41
+ };
42
+
43
+ describe('Arcana integration adapters', () => {
44
+ it('wraps a Viem-compatible wallet client', async () => {
45
+ const signTypedData = vi.fn().mockResolvedValue('0x' + '11'.repeat(65));
46
+ const walletClient = {
47
+ account: { address },
48
+ getChainId: vi.fn().mockResolvedValue(31337),
49
+ signTypedData,
50
+ };
51
+
52
+ const adapter = createViemArcanaWalletAdapter(walletClient);
53
+
54
+ await expect(adapter.getAddress()).resolves.toBe(address);
55
+ await expect(adapter.getChainId?.()).resolves.toBe(31337);
56
+ await expect(adapter.signTypedData(typedData)).resolves.toBe('0x' + '11'.repeat(65));
57
+ expect(signTypedData).toHaveBeenCalledWith(expect.objectContaining({ account: address }));
58
+ });
59
+
60
+ it('wraps an EIP-1193 provider and signs typed data v4', async () => {
61
+ const request = vi.fn(async ({ method }: { method: string }) => {
62
+ if (method === 'eth_accounts') return [address];
63
+ if (method === 'eth_chainId') return '0x7a69';
64
+ if (method === 'eth_signTypedData_v4') return '0x' + '22'.repeat(65);
65
+ throw new Error(`unexpected method ${method}`);
66
+ });
67
+
68
+ const adapter = createEip1193ArcanaWalletAdapter({ request });
69
+
70
+ await expect(adapter.getAddress()).resolves.toBe(address);
71
+ await expect(adapter.getChainId?.()).resolves.toBe(31337);
72
+ await expect(adapter.signTypedData(typedData)).resolves.toBe('0x' + '22'.repeat(65));
73
+ expect(request).toHaveBeenCalledWith(expect.objectContaining({
74
+ method: 'eth_signTypedData_v4',
75
+ params: expect.arrayContaining([address]),
76
+ }));
77
+ });
78
+
79
+ it('uses Dynamic getWalletClient when present', async () => {
80
+ const walletClient = {
81
+ account: { address },
82
+ signTypedData: vi.fn().mockResolvedValue('0x' + '33'.repeat(65)),
83
+ };
84
+
85
+ const adapter = await createDynamicArcanaWalletAdapter({
86
+ wallet: {
87
+ address,
88
+ getWalletClient: vi.fn().mockResolvedValue(walletClient),
89
+ },
90
+ });
91
+
92
+ await expect(adapter.getAddress()).resolves.toBe(address);
93
+ await expect(adapter.signTypedData(typedData)).resolves.toBe('0x' + '33'.repeat(65));
94
+ });
95
+
96
+ it('restores Dynamic Arcana device auth without registering again', async () => {
97
+ const walletClient = {
98
+ account: { address },
99
+ getChainId: vi.fn().mockResolvedValue(31337),
100
+ signTypedData: vi.fn().mockResolvedValue('0x' + '33'.repeat(65)),
101
+ };
102
+ const registerDevice = vi.fn();
103
+ const activateWallet = vi.fn();
104
+
105
+ const result = await authenticateArcanaWithDynamic({
106
+ client: {
107
+ auth: {
108
+ getDomainInfo: vi.fn().mockResolvedValue({
109
+ name: 'Arcana',
110
+ version: '1',
111
+ chainId: 31337,
112
+ verifyingContract,
113
+ }),
114
+ },
115
+ isAuthenticated: vi.fn().mockReturnValue(true),
116
+ ensureAuthenticated: vi.fn().mockResolvedValue(true),
117
+ deviceAuth: {
118
+ activateWallet,
119
+ getWalletAddress: vi.fn().mockReturnValue(address),
120
+ getUserId: vi.fn().mockReturnValue(address),
121
+ registerDevice,
122
+ },
123
+ },
124
+ walletClient,
125
+ chainId: 31337,
126
+ });
127
+
128
+ expect(result).toMatchObject({ address, userId: address, status: 'restored', restored: true });
129
+ expect(activateWallet).toHaveBeenCalledWith(address);
130
+ expect(registerDevice).not.toHaveBeenCalled();
131
+ });
132
+
133
+ it('exposes a Dynamic session helper for one-button Arcana sign-in flows', async () => {
134
+ const walletClient = {
135
+ account: { address },
136
+ getChainId: vi.fn().mockResolvedValue(31337),
137
+ signTypedData: vi.fn().mockResolvedValue('0x' + '33'.repeat(65)),
138
+ };
139
+ const registerDevice = vi.fn().mockResolvedValue({
140
+ device_id: 'dev-1',
141
+ refresh_token: 'refresh',
142
+ refresh_expires_at: 1,
143
+ access_token: 'access',
144
+ access_token_id: 'access-id',
145
+ access_expires_at: 2,
146
+ user_id: address,
147
+ });
148
+
149
+ const result = await ensureArcanaSessionWithDynamic({
150
+ client: {
151
+ auth: {
152
+ getDomainInfo: vi.fn().mockResolvedValue({
153
+ name: 'Arcana',
154
+ version: '1',
155
+ chainId: 31337,
156
+ verifyingContract,
157
+ }),
158
+ },
159
+ isAuthenticated: vi.fn().mockReturnValue(false),
160
+ ensureAuthenticated: vi.fn().mockResolvedValue(false),
161
+ deviceAuth: {
162
+ activateWallet: vi.fn(),
163
+ getWalletAddress: vi.fn().mockReturnValue(null),
164
+ getUserId: vi.fn().mockReturnValue(address),
165
+ registerDevice,
166
+ },
167
+ },
168
+ walletClient,
169
+ chainId: 31337,
170
+ deviceName: 'dynamic one button',
171
+ });
172
+
173
+ expect(result).toMatchObject({
174
+ address,
175
+ userId: address,
176
+ status: 'registered',
177
+ restored: false,
178
+ });
179
+ });
180
+
181
+ it('registers Dynamic Arcana device auth when restore is unavailable', async () => {
182
+ const walletClient = {
183
+ account: { address },
184
+ getChainId: vi.fn().mockResolvedValue(31337),
185
+ signTypedData: vi.fn().mockResolvedValue('0x' + '33'.repeat(65)),
186
+ };
187
+ const registerDevice = vi.fn().mockResolvedValue({
188
+ device_id: 'dev-1',
189
+ refresh_token: 'refresh',
190
+ refresh_expires_at: 1,
191
+ access_token: 'access',
192
+ access_token_id: 'access-id',
193
+ access_expires_at: 2,
194
+ user_id: address,
195
+ user: { id: address },
196
+ });
197
+
198
+ const result = await authenticateArcanaWithDynamic({
199
+ client: {
200
+ auth: {
201
+ getDomainInfo: vi.fn().mockResolvedValue({
202
+ name: 'Arcana',
203
+ version: '1',
204
+ chainId: 31337,
205
+ verifyingContract,
206
+ }),
207
+ },
208
+ isAuthenticated: vi.fn().mockReturnValue(false),
209
+ ensureAuthenticated: vi.fn().mockResolvedValue(false),
210
+ deviceAuth: {
211
+ activateWallet: vi.fn(),
212
+ getWalletAddress: vi.fn().mockReturnValue(null),
213
+ getUserId: vi.fn().mockReturnValue(address),
214
+ registerDevice,
215
+ },
216
+ },
217
+ walletClient,
218
+ chainId: 31337,
219
+ deviceName: 'dynamic test',
220
+ });
221
+
222
+ expect(result).toMatchObject({ address, userId: address, status: 'registered', restored: false });
223
+ expect(registerDevice).toHaveBeenCalledWith(
224
+ expect.any(Object),
225
+ address,
226
+ 31337,
227
+ verifyingContract,
228
+ 'dynamic test'
229
+ );
230
+ });
231
+
232
+ it('creates a deterministic local test adapter', async () => {
233
+ const adapter = createTestArcanaWalletAdapter();
234
+
235
+ expect(await adapter.getAddress()).toBe('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266');
236
+ expect(await adapter.getChainId?.()).toBe(31337);
237
+ await expect(adapter.signTypedData(typedData)).resolves.toMatch(/^0x[0-9a-f]+$/);
238
+ });
239
+
240
+ it('requires a ZeroDev owner signer instead of accepting a Kernel-only client', () => {
241
+ expect(() => createZeroDevArcanaWalletAdapter({
242
+ kernelClient: {
243
+ account: { address: '0x2222222222222222222222222222222222222222' },
244
+ signTypedData: vi.fn().mockResolvedValue('0x' + '44'.repeat(65)),
245
+ },
246
+ })).toThrow('ownerWalletClient');
247
+ });
248
+
249
+ it('registers Arcana device auth with adapter domain values', async () => {
250
+ const adapter = createViemArcanaWalletAdapter({
251
+ account: { address },
252
+ getChainId: vi.fn().mockResolvedValue(31337),
253
+ signTypedData: vi.fn().mockResolvedValue('0x' + '55'.repeat(65)),
254
+ });
255
+
256
+ const registerDevice = vi.fn().mockResolvedValue({
257
+ device_id: 'dev-1',
258
+ refresh_token: 'refresh',
259
+ refresh_expires_at: 1,
260
+ access_token: 'access',
261
+ access_token_id: 'access-id',
262
+ access_expires_at: 2,
263
+ user_id: address,
264
+ });
265
+
266
+ await registerArcanaDeviceWithAdapter({
267
+ auth: {
268
+ getDomainInfo: vi.fn().mockResolvedValue({
269
+ name: 'Arcana',
270
+ version: '1',
271
+ chainId: 31337,
272
+ verifyingContract,
273
+ }),
274
+ },
275
+ deviceAuth: { registerDevice },
276
+ }, adapter, { deviceName: 'test device' });
277
+
278
+ expect(registerDevice).toHaveBeenCalledWith(
279
+ adapter,
280
+ address,
281
+ 31337,
282
+ verifyingContract,
283
+ 'test device'
284
+ );
285
+ });
286
+ });
@@ -0,0 +1,56 @@
1
+ import type { ArcanaWalletAdapter, DeviceRegistrationResponse, DomainInfo } from '@arcanahq/sdk';
2
+ import { assertHexAddress } from './common.js';
3
+
4
+ export interface ArcanaClientForDeviceRegistration {
5
+ auth: {
6
+ getDomainInfo(): Promise<DomainInfo>;
7
+ };
8
+ deviceAuth?: {
9
+ registerDevice(
10
+ wallet: ArcanaWalletAdapter,
11
+ address: `0x${string}`,
12
+ chainId: number,
13
+ verifyingContract: `0x${string}`,
14
+ deviceName?: string
15
+ ): Promise<DeviceRegistrationResponse>;
16
+ };
17
+ }
18
+
19
+ export interface RegisterArcanaDeviceOptions {
20
+ chainId?: number;
21
+ verifyingContract?: `0x${string}`;
22
+ deviceName?: string;
23
+ }
24
+
25
+ export async function registerArcanaDeviceWithAdapter(
26
+ client: ArcanaClientForDeviceRegistration,
27
+ adapter: ArcanaWalletAdapter,
28
+ options: RegisterArcanaDeviceOptions = {}
29
+ ): Promise<DeviceRegistrationResponse> {
30
+ if (!client.deviceAuth) {
31
+ throw new Error('Arcana device authentication is not enabled on this client');
32
+ }
33
+
34
+ const address = await adapter.getAddress();
35
+ const domainInfo = await client.auth.getDomainInfo();
36
+ const domainVerifyingContract = domainInfo.verifyingContract || domainInfo.verifying_contract;
37
+ const verifyingContract = assertHexAddress(
38
+ options.verifyingContract ?? domainVerifyingContract,
39
+ 'Arcana auth verifying contract'
40
+ );
41
+
42
+ const adapterChainId = adapter.getChainId ? await adapter.getChainId() : undefined;
43
+ const chainId = options.chainId ?? adapterChainId ?? domainInfo.chainId;
44
+
45
+ if (chainId === undefined || !Number.isInteger(chainId)) {
46
+ throw new Error('Arcana device registration requires a chainId');
47
+ }
48
+
49
+ return client.deviceAuth.registerDevice(
50
+ adapter,
51
+ address,
52
+ chainId,
53
+ verifyingContract,
54
+ options.deviceName
55
+ );
56
+ }