accounts 0.12.2 → 0.14.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/CHANGELOG.md +35 -0
- package/README.md +6 -5
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +76 -69
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/AccessKey.d.ts +89 -106
- package/dist/core/AccessKey.d.ts.map +1 -1
- package/dist/core/AccessKey.js +230 -210
- package/dist/core/AccessKey.js.map +1 -1
- package/dist/core/Account.d.ts +0 -9
- package/dist/core/Account.d.ts.map +1 -1
- package/dist/core/Account.js +1 -13
- package/dist/core/Account.js.map +1 -1
- package/dist/core/Adapter.d.ts +24 -4
- package/dist/core/Adapter.d.ts.map +1 -1
- package/dist/core/Client.d.ts +1 -1
- package/dist/core/Client.d.ts.map +1 -1
- package/dist/core/Client.js.map +1 -1
- package/dist/core/Provider.d.ts +6 -7
- package/dist/core/Provider.d.ts.map +1 -1
- package/dist/core/Provider.js +73 -77
- package/dist/core/Provider.js.map +1 -1
- package/dist/core/Remote.d.ts +2 -0
- package/dist/core/Remote.d.ts.map +1 -1
- package/dist/core/Remote.js +3 -1
- package/dist/core/Remote.js.map +1 -1
- package/dist/core/Schema.d.ts +17 -7
- package/dist/core/Schema.d.ts.map +1 -1
- package/dist/core/adapters/dialog.d.ts.map +1 -1
- package/dist/core/adapters/dialog.js +126 -131
- package/dist/core/adapters/dialog.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +92 -105
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/core/adapters/privy.d.ts +164 -0
- package/dist/core/adapters/privy.d.ts.map +1 -0
- package/dist/core/adapters/privy.js +578 -0
- package/dist/core/adapters/privy.js.map +1 -0
- package/dist/core/adapters/turnkey.d.ts.map +1 -1
- package/dist/core/adapters/turnkey.js +136 -133
- package/dist/core/adapters/turnkey.js.map +1 -1
- package/dist/core/internal/AccessKeyTransaction.d.ts +59 -0
- package/dist/core/internal/AccessKeyTransaction.d.ts.map +1 -0
- package/dist/core/internal/AccessKeyTransaction.js +155 -0
- package/dist/core/internal/AccessKeyTransaction.js.map +1 -0
- package/dist/core/zod/rpc.d.ts +71 -4
- package/dist/core/zod/rpc.d.ts.map +1 -1
- package/dist/core/zod/rpc.js +33 -4
- package/dist/core/zod/rpc.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react-native/adapter.d.ts.map +1 -1
- package/dist/react-native/adapter.js +86 -75
- package/dist/react-native/adapter.js.map +1 -1
- package/dist/server/CliAuth.d.ts +55 -0
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +18 -1
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Kv.d.ts +19 -9
- package/dist/server/Kv.d.ts.map +1 -1
- package/dist/server/Kv.js +32 -9
- package/dist/server/Kv.js.map +1 -1
- package/dist/server/internal/handlers/auth.d.ts.map +1 -1
- package/dist/server/internal/handlers/auth.js +3 -1
- package/dist/server/internal/handlers/auth.js.map +1 -1
- package/dist/server/internal/handlers/codeAuth.js +1 -1
- package/dist/server/internal/handlers/codeAuth.js.map +1 -1
- package/dist/server/internal/handlers/exchange.js +1 -1
- package/dist/server/internal/handlers/exchange.js.map +1 -1
- package/dist/server/internal/handlers/relay.d.ts +18 -12
- package/dist/server/internal/handlers/relay.d.ts.map +1 -1
- package/dist/server/internal/handlers/relay.js +116 -3
- package/dist/server/internal/handlers/relay.js.map +1 -1
- package/dist/server/internal/handlers/webAuthn.d.ts +6 -1
- package/dist/server/internal/handlers/webAuthn.d.ts.map +1 -1
- package/dist/server/internal/handlers/webAuthn.js +16 -9
- package/dist/server/internal/handlers/webAuthn.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/{Provider.test.ts → Provider.localnet.test.ts} +49 -1
- package/src/cli/adapter.ts +95 -76
- package/src/core/AccessKey.test.ts +563 -560
- package/src/core/AccessKey.ts +327 -327
- package/src/core/Account.test.ts +6 -51
- package/src/core/Account.ts +1 -19
- package/src/core/Adapter.ts +22 -4
- package/src/core/Client.test.ts +1 -1
- package/src/core/Client.ts +1 -1
- package/src/core/Provider.connect.browser.test.ts +1 -1
- package/src/core/{Provider.test.ts → Provider.localnet.test.ts} +98 -32
- package/src/core/Provider.ts +118 -112
- package/src/core/Remote.test.ts +28 -0
- package/src/core/Remote.ts +4 -1
- package/src/core/Schema.test-d.ts +41 -0
- package/src/core/adapters/dialog.test.ts +335 -15
- package/src/core/adapters/dialog.ts +120 -127
- package/src/core/adapters/local.test.ts +1 -1
- package/src/core/adapters/local.ts +95 -119
- package/src/core/adapters/privy.test-d.ts +128 -0
- package/src/core/adapters/privy.test.ts +648 -0
- package/src/core/adapters/privy.ts +811 -0
- package/src/core/adapters/turnkey.test.ts +117 -13
- package/src/core/adapters/turnkey.ts +132 -149
- package/src/core/internal/AccessKeyTransaction.ts +243 -0
- package/src/core/{mppx.test.ts → mppx.localnet.test.ts} +32 -2
- package/src/core/zod/request.test.ts +36 -0
- package/src/core/zod/rpc.test.ts +139 -0
- package/src/core/zod/rpc.ts +44 -10
- package/src/index.ts +1 -1
- package/src/react-native/{Provider.test.ts → Provider.localnet.test.ts} +65 -0
- package/src/react-native/adapter.ts +107 -87
- package/src/server/CliAuth.test-d.ts +23 -0
- package/src/server/CliAuth.test.ts +47 -0
- package/src/server/CliAuth.ts +20 -1
- package/src/server/Handler.test-d.ts +1 -1
- package/src/server/Kv.test.ts +51 -0
- package/src/server/Kv.ts +43 -9
- package/src/server/internal/handlers/auth.test.ts +10 -5
- package/src/server/internal/handlers/auth.ts +4 -1
- package/src/server/internal/handlers/codeAuth.ts +1 -1
- package/src/server/internal/handlers/exchange.ts +1 -1
- package/src/server/internal/handlers/{relay.test.ts → relay.localnet.test.ts} +4 -1
- package/src/server/internal/handlers/relay.ts +139 -14
- package/src/server/internal/handlers/session.test.ts +1 -1
- package/src/server/internal/handlers/webAuthn.test.ts +20 -0
- package/src/server/internal/handlers/webAuthn.ts +24 -12
- /package/src/server/internal/handlers/{exchange.test.ts → exchange.localnet.test.ts} +0 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { Address as core_Address, Hex, Secp256k1, Signature } from 'ox'
|
|
2
|
+
import { decodeFunctionData } from 'viem'
|
|
3
|
+
import type { Address } from 'viem/accounts'
|
|
4
|
+
import { Abis } from 'viem/tempo'
|
|
5
|
+
import { describe, expect, test } from 'vp/test'
|
|
6
|
+
|
|
7
|
+
import * as Storage from '../Storage.js'
|
|
8
|
+
import * as Store from '../Store.js'
|
|
9
|
+
import { privy } from './privy.js'
|
|
10
|
+
|
|
11
|
+
// Deterministic test keys so addresses and signatures are reproducible across
|
|
12
|
+
// runs. Real signing is required by upcoming signer-recovery validation, and
|
|
13
|
+
// keeps the mocks honest about what the production adapter sees from Privy.
|
|
14
|
+
const privateKeyA = Hex.padLeft('0x01', 32)
|
|
15
|
+
const privateKeyB = Hex.padLeft('0x02', 32)
|
|
16
|
+
const address = core_Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: privateKeyA }))
|
|
17
|
+
const other = core_Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey: privateKeyB }))
|
|
18
|
+
|
|
19
|
+
function signWithKey(privateKey: Hex.Hex, payload: Hex.Hex): Hex.Hex {
|
|
20
|
+
const signature = Secp256k1.sign({ payload, privateKey })
|
|
21
|
+
return Signature.toHex(signature)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function privateKeyForAddress(walletAddress: string): Hex.Hex {
|
|
25
|
+
if (core_Address.from(walletAddress) === address) return privateKeyA
|
|
26
|
+
if (core_Address.from(walletAddress) === other) return privateKeyB
|
|
27
|
+
throw new Error(`No test private key for ${walletAddress}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('privy', () => {
|
|
31
|
+
test('default: createAccount delegates registration and signs the requested digest', async () => {
|
|
32
|
+
const { adapter, client } = setup()
|
|
33
|
+
|
|
34
|
+
const result = await adapter.actions.createAccount(
|
|
35
|
+
{ digest: '0x1234', name: 'Ada' },
|
|
36
|
+
{ method: 'wallet_connect', params: undefined },
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
expect(client.initCalls).toMatchInlineSnapshot(`1`)
|
|
40
|
+
expect(client.signPayloads).toMatchInlineSnapshot(`
|
|
41
|
+
[
|
|
42
|
+
"0x1234",
|
|
43
|
+
]
|
|
44
|
+
`)
|
|
45
|
+
expect(result).toMatchInlineSnapshot(`
|
|
46
|
+
{
|
|
47
|
+
"accounts": [
|
|
48
|
+
{
|
|
49
|
+
"address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
|
|
50
|
+
"label": "Ada",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
"signature": "0xced9d002f487622c7e218274065c327bdfe274ea7da91349bb48fe7c4495baeb71cc6b2f9b3d5f34e5b404cec0ed0dcb085f990a7b7a7f4cb81a5e8abb76aa981b",
|
|
54
|
+
}
|
|
55
|
+
`)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('default: createAccount falls back to loadAccounts when not provided', async () => {
|
|
59
|
+
const { adapter, client } = setup({ createAccount: false })
|
|
60
|
+
|
|
61
|
+
const result = await adapter.actions.createAccount(
|
|
62
|
+
{ digest: '0x1234', name: 'Ada' },
|
|
63
|
+
{ method: 'wallet_connect', params: undefined },
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expect(client.createCalls).toMatchInlineSnapshot(`0`)
|
|
67
|
+
expect(client.loadCalls).toMatchInlineSnapshot(`1`)
|
|
68
|
+
expect(client.signPayloads).toMatchInlineSnapshot(`
|
|
69
|
+
[
|
|
70
|
+
"0x1234",
|
|
71
|
+
]
|
|
72
|
+
`)
|
|
73
|
+
expect(result.accounts).toMatchInlineSnapshot(`
|
|
74
|
+
[
|
|
75
|
+
{
|
|
76
|
+
"address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
|
|
77
|
+
"label": "Ada",
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
`)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('default: loadAccounts delegates login and caches embedded wallets for signing', async () => {
|
|
84
|
+
const { adapter, client } = setup()
|
|
85
|
+
|
|
86
|
+
await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
|
|
87
|
+
const result = await adapter.actions.signPersonalMessage(
|
|
88
|
+
{ address, data: '0x68656c6c6f' },
|
|
89
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
expect(client.loadCalls).toMatchInlineSnapshot(`1`)
|
|
93
|
+
expect(client.signWith).toMatchInlineSnapshot(`
|
|
94
|
+
[
|
|
95
|
+
"0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
|
|
96
|
+
]
|
|
97
|
+
`)
|
|
98
|
+
expect(result).toMatchInlineSnapshot(
|
|
99
|
+
`"0xe5ddc160e4c8f92de507c7db9b982d4f9b7197bfa421864aeadc586bc96b09ae0ba0c5b131650ae4994cff1839341d00f3735ef5abc62ac8fe2cf50f65208e2a1b"`,
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('default: loadAccounts can provision an external access key', async () => {
|
|
104
|
+
const { adapter, client } = setup()
|
|
105
|
+
|
|
106
|
+
const result = await adapter.actions.loadAccounts(
|
|
107
|
+
{
|
|
108
|
+
authorizeAccessKey: {
|
|
109
|
+
address: other,
|
|
110
|
+
expiry: 123,
|
|
111
|
+
keyType: 'secp256k1',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{ method: 'wallet_connect', params: undefined },
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
expect(client.signPayloads).toMatchInlineSnapshot(`
|
|
118
|
+
[
|
|
119
|
+
"0xe77ac2b1d13a90cbd8c4912ff18d0d044cc89c5c6781941001640b8d251f3783",
|
|
120
|
+
]
|
|
121
|
+
`)
|
|
122
|
+
expect(result).toMatchInlineSnapshot(`
|
|
123
|
+
{
|
|
124
|
+
"accounts": [
|
|
125
|
+
{
|
|
126
|
+
"address": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
"keyAuthorization": {
|
|
130
|
+
"chainId": "0x1",
|
|
131
|
+
"expiry": "0x7b",
|
|
132
|
+
"keyId": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
|
|
133
|
+
"keyType": "secp256k1",
|
|
134
|
+
"limits": undefined,
|
|
135
|
+
"signature": {
|
|
136
|
+
"r": "0xb364cd8e50555239adf9f7d655b018ea386764d44ed9b56e894f4a101f0b1a6b",
|
|
137
|
+
"s": "0x4910cc8497358eb73a08df09c9cfb2618e3c949b3847ab310ad7ab0d76a9c624",
|
|
138
|
+
"type": "secp256k1",
|
|
139
|
+
"yParity": "0x1",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
"signature": undefined,
|
|
143
|
+
}
|
|
144
|
+
`)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('default: authorizeAccessKey signs with the connected Privy account', async () => {
|
|
148
|
+
const { adapter, client, store } = setup()
|
|
149
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
150
|
+
|
|
151
|
+
const result = await adapter.actions.authorizeAccessKey!(
|
|
152
|
+
{
|
|
153
|
+
address: other,
|
|
154
|
+
expiry: 123,
|
|
155
|
+
keyType: 'secp256k1',
|
|
156
|
+
},
|
|
157
|
+
{ method: 'wallet_authorizeAccessKey', params: [{ expiry: 123 }] },
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
expect(client.loadCalls).toMatchInlineSnapshot(`0`)
|
|
161
|
+
expect(client.restoreCalls).toMatchInlineSnapshot(`1`)
|
|
162
|
+
expect(result).toMatchInlineSnapshot(`
|
|
163
|
+
{
|
|
164
|
+
"keyAuthorization": {
|
|
165
|
+
"chainId": "0x1",
|
|
166
|
+
"expiry": "0x7b",
|
|
167
|
+
"keyId": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
|
|
168
|
+
"keyType": "secp256k1",
|
|
169
|
+
"limits": undefined,
|
|
170
|
+
"signature": {
|
|
171
|
+
"r": "0xb364cd8e50555239adf9f7d655b018ea386764d44ed9b56e894f4a101f0b1a6b",
|
|
172
|
+
"s": "0x4910cc8497358eb73a08df09c9cfb2618e3c949b3847ab310ad7ab0d76a9c624",
|
|
173
|
+
"type": "secp256k1",
|
|
174
|
+
"yParity": "0x1",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
"rootAddress": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
|
|
178
|
+
}
|
|
179
|
+
`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('error: secp256k1 access key requires external key material', async () => {
|
|
183
|
+
const { adapter, store } = setup()
|
|
184
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
185
|
+
|
|
186
|
+
await expect(
|
|
187
|
+
adapter.actions.authorizeAccessKey!(
|
|
188
|
+
{ expiry: 123, keyType: 'secp256k1' },
|
|
189
|
+
{ method: 'wallet_authorizeAccessKey', params: [{ expiry: 123, keyType: 'secp256k1' }] },
|
|
190
|
+
),
|
|
191
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
192
|
+
`[RpcResponse.InvalidParamsError: \`keyType: "secp256k1"\` requires externally generated key material; provide \`publicKey\` or \`address\`.]`,
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('default: revokeAccessKey revokes with the connected Privy account', async () => {
|
|
197
|
+
const { adapter, client, store } = setup()
|
|
198
|
+
store.setState({
|
|
199
|
+
accounts: [{ address }],
|
|
200
|
+
activeAccount: 0,
|
|
201
|
+
accessKeys: [
|
|
202
|
+
{
|
|
203
|
+
access: address,
|
|
204
|
+
address: other,
|
|
205
|
+
chainId: 1,
|
|
206
|
+
keyType: 'secp256k1',
|
|
207
|
+
} as never,
|
|
208
|
+
],
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
await adapter.actions.revokeAccessKey!(
|
|
212
|
+
{ accessKeyAddress: other, address },
|
|
213
|
+
{ method: 'wallet_revokeAccessKey', params: [{ accessKeyAddress: other, address }] },
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
const transaction = client.transactions[0] as
|
|
217
|
+
| { account: { address: Address }; data: Hex.Hex; to: Address }
|
|
218
|
+
| undefined
|
|
219
|
+
const decoded = transaction
|
|
220
|
+
? decodeFunctionData({ abi: Abis.accountKeychain, data: transaction.data })
|
|
221
|
+
: undefined
|
|
222
|
+
expect(
|
|
223
|
+
transaction &&
|
|
224
|
+
decoded && {
|
|
225
|
+
account: transaction.account.address,
|
|
226
|
+
args: decoded.args,
|
|
227
|
+
functionName: decoded.functionName,
|
|
228
|
+
to: transaction.to,
|
|
229
|
+
},
|
|
230
|
+
).toMatchInlineSnapshot(`
|
|
231
|
+
{
|
|
232
|
+
"account": "0x7e5f4552091a69125d5dfcb7b8c2659029395bdf",
|
|
233
|
+
"args": [
|
|
234
|
+
"0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF",
|
|
235
|
+
],
|
|
236
|
+
"functionName": "revokeKey",
|
|
237
|
+
"to": "0xaAAAaaAA00000000000000000000000000000000",
|
|
238
|
+
}
|
|
239
|
+
`)
|
|
240
|
+
expect(store.getState().accessKeys).toMatchInlineSnapshot(`[]`)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('behavior: signing silently restores wallet accounts via the Privy SDK', async () => {
|
|
244
|
+
const { adapter, client, store } = setup()
|
|
245
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
246
|
+
|
|
247
|
+
await adapter.actions.signPersonalMessage(
|
|
248
|
+
{ address, data: '0x68656c6c6f' },
|
|
249
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
expect(client.restoreCalls).toMatchInlineSnapshot(`1`)
|
|
253
|
+
expect(client.loadCalls).toMatchInlineSnapshot(`0`)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('behavior: silent restore does not connect accounts when the provider store is empty', async () => {
|
|
257
|
+
const { adapter, client } = setup()
|
|
258
|
+
|
|
259
|
+
await expect(
|
|
260
|
+
adapter.actions.signPersonalMessage(
|
|
261
|
+
{ address, data: '0x68656c6c6f' },
|
|
262
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
263
|
+
),
|
|
264
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: No Privy account connected.]')
|
|
265
|
+
|
|
266
|
+
expect(client.loadCalls).toMatchInlineSnapshot(`0`)
|
|
267
|
+
expect(client.restoreCalls).toMatchInlineSnapshot(`0`)
|
|
268
|
+
expect(client.signPayloads).toMatchInlineSnapshot(`[]`)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test('behavior: silent restore only reconnects persisted provider accounts', async () => {
|
|
272
|
+
const { adapter, client, store } = setup()
|
|
273
|
+
client.addWallet(other)
|
|
274
|
+
store.setState({ accounts: [{ address: other }], activeAccount: 0 })
|
|
275
|
+
|
|
276
|
+
await adapter.actions.signPersonalMessage(
|
|
277
|
+
{ address: other, data: '0x68656c6c6f' },
|
|
278
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', other] },
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
expect(client.signWith).toMatchInlineSnapshot(`
|
|
282
|
+
[
|
|
283
|
+
"0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
|
|
284
|
+
]
|
|
285
|
+
`)
|
|
286
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`
|
|
287
|
+
[
|
|
288
|
+
{
|
|
289
|
+
"address": "0x2b5ad5c4795c026514f8317c7a215e218dccd6cf",
|
|
290
|
+
},
|
|
291
|
+
]
|
|
292
|
+
`)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('behavior: expired sessions clear provider accounts', async () => {
|
|
296
|
+
const { adapter, client, store } = setup({ token: null })
|
|
297
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
298
|
+
|
|
299
|
+
await expect(
|
|
300
|
+
adapter.actions.signPersonalMessage(
|
|
301
|
+
{ address, data: '0x68656c6c6f' },
|
|
302
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
303
|
+
),
|
|
304
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')
|
|
305
|
+
|
|
306
|
+
expect(client.signPayloads).toMatchInlineSnapshot(`[]`)
|
|
307
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('behavior: server session errors clear provider accounts', async () => {
|
|
311
|
+
const { adapter, store } = setup({ signError: { code: 'embedded_wallet_request_error' } })
|
|
312
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
313
|
+
|
|
314
|
+
await expect(
|
|
315
|
+
adapter.actions.signPersonalMessage(
|
|
316
|
+
{ address, data: '0x68656c6c6f' },
|
|
317
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
318
|
+
),
|
|
319
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')
|
|
320
|
+
|
|
321
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('behavior: session errors are recognized via fuzzy code match', async () => {
|
|
325
|
+
const { adapter, store } = setup({ signError: { code: 'session_invalid_token' } })
|
|
326
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
327
|
+
|
|
328
|
+
await expect(
|
|
329
|
+
adapter.actions.signPersonalMessage(
|
|
330
|
+
{ address, data: '0x68656c6c6f' },
|
|
331
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
332
|
+
),
|
|
333
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')
|
|
334
|
+
|
|
335
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('behavior: session errors are recognized via nested cause messages', async () => {
|
|
339
|
+
const inner = new Error('User must be logged in to sign.')
|
|
340
|
+
const outer = new Error('Wallet operation failed', { cause: inner })
|
|
341
|
+
const { adapter, store } = setup({ signError: outer })
|
|
342
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
343
|
+
|
|
344
|
+
await expect(
|
|
345
|
+
adapter.actions.signPersonalMessage(
|
|
346
|
+
{ address, data: '0x68656c6c6f' },
|
|
347
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
348
|
+
),
|
|
349
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')
|
|
350
|
+
|
|
351
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('behavior: session errors are recognized via message fallback', async () => {
|
|
355
|
+
const { adapter, store } = setup({
|
|
356
|
+
signError: Object.assign(new Error('User must be logged in to sign.'), {}),
|
|
357
|
+
})
|
|
358
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
359
|
+
|
|
360
|
+
await expect(
|
|
361
|
+
adapter.actions.signPersonalMessage(
|
|
362
|
+
{ address, data: '0x68656c6c6f' },
|
|
363
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
364
|
+
),
|
|
365
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')
|
|
366
|
+
|
|
367
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
test('behavior: silent restore clears stale persisted accounts when Privy no longer has them', async () => {
|
|
371
|
+
const { adapter, client, store } = setup()
|
|
372
|
+
// Persisted address that is NOT linked on the Privy user.
|
|
373
|
+
store.setState({ accounts: [{ address: other }], activeAccount: 0 })
|
|
374
|
+
|
|
375
|
+
await expect(
|
|
376
|
+
adapter.actions.signPersonalMessage(
|
|
377
|
+
{ address: other, data: '0x68656c6c6f' },
|
|
378
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', other] },
|
|
379
|
+
),
|
|
380
|
+
).rejects.toMatchInlineSnapshot(
|
|
381
|
+
'[Provider.DisconnectedError: Privy session no longer matches persisted accounts.]',
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
expect(client.signPayloads).toMatchInlineSnapshot(`[]`)
|
|
385
|
+
// Stale persisted accounts are wiped so the adapter and store agree.
|
|
386
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
test('error: silent restore rejects non-hex secp256k1_sign results', async () => {
|
|
390
|
+
const { adapter, store } = setup({ signResult: 'not-hex' })
|
|
391
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
392
|
+
|
|
393
|
+
await expect(
|
|
394
|
+
adapter.actions.signPersonalMessage(
|
|
395
|
+
{ address, data: '0x68656c6c6f' },
|
|
396
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
397
|
+
),
|
|
398
|
+
).rejects.toMatchInlineSnapshot(
|
|
399
|
+
'[ProviderRpcError: Privy provider returned a non-hex secp256k1_sign result.]',
|
|
400
|
+
)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
test('error: app-returned wallet with malformed address is rejected at connect', async () => {
|
|
404
|
+
const { adapter, client } = setup()
|
|
405
|
+
client.wallets = [client.makeWallet('0xnot-an-address')]
|
|
406
|
+
|
|
407
|
+
await expect(
|
|
408
|
+
adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined }),
|
|
409
|
+
).rejects.toThrowError(/Address.*invalid/i)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('error: malformed secp256k1_sign result is rejected by signer recovery', async () => {
|
|
413
|
+
const { adapter } = setup({ signResult: '0x1234' })
|
|
414
|
+
await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
|
|
415
|
+
|
|
416
|
+
await expect(
|
|
417
|
+
adapter.actions.signPersonalMessage(
|
|
418
|
+
{ address, data: '0x68656c6c6f' },
|
|
419
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
420
|
+
),
|
|
421
|
+
).rejects.toMatchInlineSnapshot(
|
|
422
|
+
`[Provider.UnauthorizedError: Privy provider returned a signature for "unknown" that does not match the requested wallet "${address}".]`,
|
|
423
|
+
)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test('error: signing for an unconnected address while others are connected throws Unauthorized', async () => {
|
|
427
|
+
const { adapter } = setup()
|
|
428
|
+
await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
|
|
429
|
+
|
|
430
|
+
await expect(
|
|
431
|
+
adapter.actions.signPersonalMessage(
|
|
432
|
+
{ address: other, data: '0x68656c6c6f' },
|
|
433
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', other] },
|
|
434
|
+
),
|
|
435
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
436
|
+
`[Provider.UnauthorizedError: Account "${other}" not found.]`,
|
|
437
|
+
)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test('error: unsupported secp256k1_sign maps to UnsupportedMethodError', async () => {
|
|
441
|
+
const { adapter, store } = setup({ signError: { code: 4200, message: 'Method not supported' } })
|
|
442
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
443
|
+
|
|
444
|
+
await expect(
|
|
445
|
+
adapter.actions.signPersonalMessage(
|
|
446
|
+
{ address, data: '0x68656c6c6f' },
|
|
447
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
448
|
+
),
|
|
449
|
+
).rejects.toMatchInlineSnapshot(
|
|
450
|
+
'[Provider.UnsupportedMethodError: Privy adapter requires raw secp256k1 hash signing via `secp256k1_sign` for Tempo transactions and access keys.]',
|
|
451
|
+
)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('disconnect: clears provider accounts and logs the user out of Privy', async () => {
|
|
455
|
+
const { adapter, client, store } = setup()
|
|
456
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
457
|
+
|
|
458
|
+
await adapter.actions.disconnect!()
|
|
459
|
+
|
|
460
|
+
expect(client.logoutCalls).toMatchInlineSnapshot(`1`)
|
|
461
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
test('behavior: restore surfaces silent-restore session errors as `Privy session expired.`', async () => {
|
|
465
|
+
const { adapter, store } = setup({
|
|
466
|
+
restoreError: Object.assign(new Error('boom'), { code: 'session_expired' }),
|
|
467
|
+
})
|
|
468
|
+
store.setState({ accounts: [{ address }], activeAccount: 0 })
|
|
469
|
+
|
|
470
|
+
await expect(
|
|
471
|
+
adapter.actions.signPersonalMessage(
|
|
472
|
+
{ address, data: '0x68656c6c6f' },
|
|
473
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
474
|
+
),
|
|
475
|
+
).rejects.toMatchInlineSnapshot('[Provider.DisconnectedError: Privy session expired.]')
|
|
476
|
+
|
|
477
|
+
expect(store.getState().accounts).toMatchInlineSnapshot(`[]`)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
test('error: signature recovered from a different key is rejected as Unauthorized', async () => {
|
|
481
|
+
const { adapter } = setup({ signWithPrivateKey: privateKeyB })
|
|
482
|
+
await adapter.actions.loadAccounts(undefined, { method: 'wallet_connect', params: undefined })
|
|
483
|
+
|
|
484
|
+
await expect(
|
|
485
|
+
adapter.actions.signPersonalMessage(
|
|
486
|
+
{ address, data: '0x68656c6c6f' },
|
|
487
|
+
{ method: 'personal_sign', params: ['0x68656c6c6f', address] },
|
|
488
|
+
),
|
|
489
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
490
|
+
`[Provider.UnauthorizedError: Privy provider returned a signature for "${other}" that does not match the requested wallet "${address}".]`,
|
|
491
|
+
)
|
|
492
|
+
})
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
function setup(options: setup.Options = {}) {
|
|
496
|
+
const storage = Storage.memory()
|
|
497
|
+
const store = Store.create({ chainId: 1, storage })
|
|
498
|
+
const client = createClient(options)
|
|
499
|
+
const adapter = privy({
|
|
500
|
+
client,
|
|
501
|
+
...(options.createAccount === false
|
|
502
|
+
? {}
|
|
503
|
+
: {
|
|
504
|
+
createAccount: async () => {
|
|
505
|
+
client.createCalls++
|
|
506
|
+
return undefined
|
|
507
|
+
},
|
|
508
|
+
}),
|
|
509
|
+
loadAccounts: async () => {
|
|
510
|
+
client.loadCalls++
|
|
511
|
+
return undefined
|
|
512
|
+
},
|
|
513
|
+
})({
|
|
514
|
+
getAccount: (() => {
|
|
515
|
+
throw new Error('not implemented')
|
|
516
|
+
}) as never,
|
|
517
|
+
getClient: (() => ({
|
|
518
|
+
chain: { id: 1 },
|
|
519
|
+
sendTransaction: async (parameters: unknown) => {
|
|
520
|
+
client.transactions.push(parameters)
|
|
521
|
+
return Hex.padLeft('0x1', 32)
|
|
522
|
+
},
|
|
523
|
+
})) as never,
|
|
524
|
+
storage,
|
|
525
|
+
store,
|
|
526
|
+
})
|
|
527
|
+
return { adapter, client, store }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
declare namespace setup {
|
|
531
|
+
type Options = {
|
|
532
|
+
/** Pass `false` to omit the adapter's `createAccount` callback (tests fallback to `loadAccounts`). */
|
|
533
|
+
createAccount?: false | undefined
|
|
534
|
+
/** Make the mock client's `user.get` throw, to test restore-side session errors. */
|
|
535
|
+
restoreError?: unknown
|
|
536
|
+
token?: string | null | undefined
|
|
537
|
+
signError?: unknown
|
|
538
|
+
/** Override the value returned by the embedded provider's `secp256k1_sign`. */
|
|
539
|
+
signResult?: unknown
|
|
540
|
+
/** Force the test wallet to sign with this private key (for wrong-signer tests). */
|
|
541
|
+
signWithPrivateKey?: Hex.Hex | undefined
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
type MockClient = privy.Client & {
|
|
546
|
+
createCalls: number
|
|
547
|
+
initCalls: number
|
|
548
|
+
loadCalls: number
|
|
549
|
+
logoutCalls: number
|
|
550
|
+
logoutWith: (string | undefined)[]
|
|
551
|
+
restoreCalls: number
|
|
552
|
+
signPayloads: Hex.Hex[]
|
|
553
|
+
signWith: string[]
|
|
554
|
+
transactions: unknown[]
|
|
555
|
+
wallets: privy.EmbeddedWallet[]
|
|
556
|
+
makeWallet: (address: string) => privy.EmbeddedWallet
|
|
557
|
+
addWallet: (address: string) => void
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function createClient(options: setup.Options = {}) {
|
|
561
|
+
const client: MockClient = {
|
|
562
|
+
createCalls: 0,
|
|
563
|
+
initCalls: 0,
|
|
564
|
+
loadCalls: 0,
|
|
565
|
+
logoutCalls: 0,
|
|
566
|
+
logoutWith: [] as (string | undefined)[],
|
|
567
|
+
restoreCalls: 0,
|
|
568
|
+
signPayloads: [] as Hex.Hex[],
|
|
569
|
+
signWith: [] as string[],
|
|
570
|
+
transactions: [] as unknown[],
|
|
571
|
+
wallets: [] as privy.EmbeddedWallet[],
|
|
572
|
+
makeWallet(address: string): privy.EmbeddedWallet {
|
|
573
|
+
return {
|
|
574
|
+
address,
|
|
575
|
+
provider: {
|
|
576
|
+
async request(req: {
|
|
577
|
+
method: string
|
|
578
|
+
params?: readonly unknown[] | undefined
|
|
579
|
+
}): Promise<unknown> {
|
|
580
|
+
if (req.method !== 'secp256k1_sign') throw new Error(`unexpected method: ${req.method}`)
|
|
581
|
+
if (options.signError) throw options.signError
|
|
582
|
+
const hash = (req.params as readonly Hex.Hex[])[0] as Hex.Hex
|
|
583
|
+
client.signPayloads.push(hash)
|
|
584
|
+
client.signWith.push(address)
|
|
585
|
+
if (options.signResult !== undefined) return options.signResult
|
|
586
|
+
const privateKey =
|
|
587
|
+
options.signWithPrivateKey ??
|
|
588
|
+
(() => {
|
|
589
|
+
try {
|
|
590
|
+
return privateKeyForAddress(address)
|
|
591
|
+
} catch {
|
|
592
|
+
return privateKeyA
|
|
593
|
+
}
|
|
594
|
+
})()
|
|
595
|
+
return signWithKey(privateKey, hash)
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
/** Adds an embedded wallet so silent restore (`user.get`) returns it. */
|
|
601
|
+
addWallet(address: string) {
|
|
602
|
+
client.wallets.push(client.makeWallet(address))
|
|
603
|
+
},
|
|
604
|
+
auth: {
|
|
605
|
+
logout(parameters?: { userId: string } | undefined) {
|
|
606
|
+
client.logoutCalls++
|
|
607
|
+
client.logoutWith.push(parameters?.userId)
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
embeddedWallet: {
|
|
611
|
+
async getEthereumProvider({ wallet }) {
|
|
612
|
+
const existing = client.wallets.find(
|
|
613
|
+
(w) => core_Address.from(w.address) === core_Address.from(wallet.address as string),
|
|
614
|
+
)
|
|
615
|
+
return (existing ?? client.makeWallet(wallet.address as string)).provider
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
async getAccessToken() {
|
|
619
|
+
return options.token === undefined ? 'token' : options.token
|
|
620
|
+
},
|
|
621
|
+
initialize() {
|
|
622
|
+
client.initCalls++
|
|
623
|
+
},
|
|
624
|
+
user: {
|
|
625
|
+
async get() {
|
|
626
|
+
client.restoreCalls++
|
|
627
|
+
if (options.restoreError) throw options.restoreError
|
|
628
|
+
return {
|
|
629
|
+
user: {
|
|
630
|
+
id: 'user_1',
|
|
631
|
+
linked_accounts: client.wallets.map((wallet, index) => ({
|
|
632
|
+
address: wallet.address,
|
|
633
|
+
chain_type: 'ethereum',
|
|
634
|
+
connector_type: 'embedded',
|
|
635
|
+
type: 'wallet',
|
|
636
|
+
wallet_client_type: 'privy',
|
|
637
|
+
wallet_index: index,
|
|
638
|
+
})),
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
client.addWallet(address)
|
|
646
|
+
|
|
647
|
+
return client
|
|
648
|
+
}
|