@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/README.md +287 -0
- package/dist/authenticate.d.ts +15 -0
- package/dist/authenticate.js +16 -0
- package/dist/common.d.ts +31 -0
- package/dist/common.js +113 -0
- package/dist/dynamic-zerodev.d.ts +8 -0
- package/dist/dynamic-zerodev.js +7 -0
- package/dist/dynamic.d.ts +42 -0
- package/dist/dynamic.js +64 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +6 -0
- package/dist/privy-zerodev.d.ts +8 -0
- package/dist/privy-zerodev.js +7 -0
- package/dist/privy.d.ts +14 -0
- package/dist/privy.js +21 -0
- package/dist/test.d.ts +12 -0
- package/dist/test.js +25 -0
- package/dist/zerodev.d.ts +32 -0
- package/dist/zerodev.js +27 -0
- package/package.json +113 -0
- package/src/__tests__/adapters.test.ts +286 -0
- package/src/authenticate.ts +56 -0
- package/src/common.ts +162 -0
- package/src/dynamic-zerodev.ts +15 -0
- package/src/dynamic.ts +135 -0
- package/src/index.ts +48 -0
- package/src/privy-zerodev.ts +15 -0
- package/src/privy.ts +48 -0
- package/src/test.ts +43 -0
- package/src/zerodev.ts +73 -0
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;
|
package/dist/zerodev.js
ADDED
|
@@ -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
|
+
}
|