Skip to content

Commit 6384992

Browse files
feat: add ethauth option for dapp client connect
1 parent 49d8a2f commit 6384992

6 files changed

Lines changed: 193 additions & 0 deletions

File tree

packages/wallet/dapp-client/src/ChainSessionManager.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ import {
3737
TransportMode,
3838
GuardConfig,
3939
CreateNewSessionPayload,
40+
EthAuthSettings,
4041
ModifyExplicitSessionPayload,
4142
SessionResponse,
4243
AddExplicitSessionPayload,
4344
FeeOption,
4445
OperationFailedStatus,
4546
OperationStatus,
47+
ETHAuthProof,
4648
} from './types/index.js'
4749
import { CACHE_DB_NAME, VALUE_FORWARDER_ADDRESS } from './utils/constants.js'
4850
import { ExplicitSession, ImplicitSession, ExplicitSessionConfig } from './index.js'
@@ -285,6 +287,7 @@ export class ChainSessionManager {
285287
preferredLoginMethod?: LoginMethod
286288
email?: string
287289
includeImplicitSession?: boolean
290+
ethAuth?: EthAuthSettings
288291
} = {},
289292
): Promise<void> {
290293
if (this.isInitialized) {
@@ -311,6 +314,7 @@ export class ChainSessionManager {
311314
origin,
312315
session: completeSession as ExplicitSession | undefined,
313316
includeImplicitSession: options.includeImplicitSession ?? false,
317+
ethAuth: options.ethAuth,
314318
preferredLoginMethod: options.preferredLoginMethod,
315319
email: options.preferredLoginMethod === 'email' ? options.email : undefined,
316320
}
@@ -377,6 +381,10 @@ export class ChainSessionManager {
377381
this.guard = guard
378382
}
379383

384+
if (payload.ethAuth) {
385+
await this._saveEthAuthProofIfProvided(connectResponse.ethAuthProof)
386+
}
387+
380388
if (this.transport.mode === TransportMode.POPUP) {
381389
this.transport.closeWallet()
382390
}
@@ -596,6 +604,10 @@ export class ChainSessionManager {
596604
this.userEmail = userEmail ?? null
597605
this.guard = guard
598606
}
607+
608+
if (savedPayload?.ethAuth) {
609+
await this._saveEthAuthProofIfProvided(connectResponse.ethAuthProof)
610+
}
599611
} else if (response.action === RequestActionType.ADD_EXPLICIT_SESSION) {
600612
if (!this.walletAddress || !Address.isEqual(receivedAddress, this.walletAddress)) {
601613
throw new InitializationError('Received an explicit session for a wallet that is not active.')
@@ -1104,6 +1116,13 @@ export class ChainSessionManager {
11041116
await this.sequenceStorage.clearSessionlessConnection()
11051117
}
11061118

1119+
private async _saveEthAuthProofIfProvided(ethAuthProof?: ETHAuthProof): Promise<void> {
1120+
if (!ethAuthProof) {
1121+
return
1122+
}
1123+
await this.sequenceStorage.saveEthAuthProof(ethAuthProof)
1124+
}
1125+
11071126
private _getCachedSignedCall(calls: Payload.Call[]): { to: Address.Address; data: Hex.Hex } | null {
11081127
if (!this.lastSignedCallCache) {
11091128
return null

packages/wallet/dapp-client/src/DappClient.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ import {
1414
GetFeeTokensResponse,
1515
GuardConfig,
1616
LoginMethod,
17+
EthAuthSettings,
1718
RandomPrivateKeyFn,
1819
RequestActionType,
20+
ETHAuthProof,
1921
SendWalletTransactionPayload,
2022
SequenceSessionStorage,
2123
SignMessagePayload,
@@ -407,6 +409,13 @@ export class DappClient {
407409
}
408410
}
409411

412+
/**
413+
* Returns the latest persisted ETHAuth proof, if one has been received from the wallet.
414+
*/
415+
public async getEthAuthProof(): Promise<ETHAuthProof | null> {
416+
return this.sequenceStorage.getEthAuthProof()
417+
}
418+
410419
/**
411420
* Restores a sessionless connection that was previously persisted via {@link disconnect} or a connect flow.
412421
* @returns A promise that resolves to true if a sessionless connection was applied.
@@ -559,6 +568,7 @@ export class DappClient {
559568
preferredLoginMethod?: LoginMethod
560569
email?: string
561570
includeImplicitSession?: boolean
571+
ethAuth?: EthAuthSettings
562572
} = {},
563573
): Promise<void> {
564574
if (this.isInitialized) {
@@ -614,6 +624,7 @@ export class DappClient {
614624
preferredLoginMethod?: LoginMethod
615625
email?: string
616626
includeImplicitSession?: boolean
627+
ethAuth?: EthAuthSettings
617628
} = {},
618629
): Promise<void> {
619630
if (!this.isInitialized || !this.hasSessionlessConnection || !this.walletAddress) {

packages/wallet/dapp-client/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export type {
2424
FeeToken,
2525
FeeOption,
2626
TransportMessage,
27+
EthAuthSettings,
28+
ETHAuthProof,
2729
} from './types/index.js'
2830
export { RequestActionType, TransportMode, MessageType } from './types/index.js'
2931
export {

packages/wallet/dapp-client/src/types/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,32 @@ export interface GuardConfig {
2828
moduleAddresses: Map<Address.Address, Address.Address>
2929
}
3030

31+
export interface EthAuthSettings {
32+
app?: string
33+
/** expiry number (in seconds) that is used for ETHAuth proof. Default is 1 week in seconds. */
34+
expiry?: number
35+
/** origin hint of the dapp's host opening the wallet. This value will automatically
36+
* be determined and verified for integrity, and can be omitted. */
37+
origin?: string
38+
/** authorizeNonce is an optional number to be passed as ETHAuth's nonce claim for replay protection. **/
39+
nonce?: number
40+
}
41+
42+
export interface ETHAuthProof {
43+
// eip712 typed-data payload for ETHAuth domain as input
44+
typedData: Payload.TypedDataToSign
45+
46+
// signature encoded in an ETHAuth proof string
47+
ewtString: string
48+
}
49+
3150
// --- Payloads for Transport ---
3251

3352
export interface CreateNewSessionPayload {
3453
origin?: string
3554
session?: ExplicitSession
3655
includeImplicitSession?: boolean
56+
ethAuth?: EthAuthSettings
3757
preferredLoginMethod?: LoginMethod
3858
email?: string
3959
}
@@ -81,6 +101,7 @@ export interface CreateNewSessionResponse {
81101
userEmail?: string
82102
loginMethod?: LoginMethod
83103
guard?: GuardConfig
104+
ethAuthProof?: ETHAuthProof
84105
}
85106

86107
export interface SignatureResponse {

packages/wallet/dapp-client/src/utils/storage.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
SignMessagePayload,
66
SignTypedDataPayload,
77
GuardConfig,
8+
ETHAuthProof,
89
SendWalletTransactionPayload,
910
ModifyExplicitSessionPayload,
1011
CreateNewSessionPayload,
@@ -81,6 +82,10 @@ export interface SequenceStorage {
8182
getSessionlessConnection(): Promise<SessionlessConnectionData | null>
8283
clearSessionlessConnection(): Promise<void>
8384

85+
saveEthAuthProof(proof: ETHAuthProof): Promise<void>
86+
getEthAuthProof(): Promise<ETHAuthProof | null>
87+
clearEthAuthProof(): Promise<void>
88+
8489
saveSessionlessConnectionSnapshot?(sessionData: SessionlessConnectionData): Promise<void>
8590
getSessionlessConnectionSnapshot?(): Promise<SessionlessConnectionData | null>
8691
clearSessionlessConnectionSnapshot?(): Promise<void>
@@ -94,6 +99,7 @@ const STORE_NAME = 'userKeys'
9499
const IMPLICIT_SESSIONS_IDB_KEY = 'SequenceImplicitSession'
95100
const EXPLICIT_SESSIONS_IDB_KEY = 'SequenceExplicitSession'
96101
const SESSIONLESS_CONNECTION_IDB_KEY = 'SequenceSessionlessConnection'
102+
const ETH_AUTH_PROOF_IDB_KEY = 'SequenceEthAuthProof'
97103
const SESSIONLESS_CONNECTION_SNAPSHOT_IDB_KEY = 'SequenceSessionlessConnectionSnapshot'
98104

99105
const PENDING_REDIRECT_REQUEST_KEY = 'SequencePendingRedirect'
@@ -305,6 +311,15 @@ export class WebStorage implements SequenceStorage {
305311
}
306312
}
307313

314+
async saveEthAuthProof(proof: ETHAuthProof): Promise<void> {
315+
try {
316+
await this.setIDBItem(ETH_AUTH_PROOF_IDB_KEY, proof)
317+
} catch (error) {
318+
console.error('Failed to save ETHAuth proof:', error)
319+
throw error
320+
}
321+
}
322+
308323
async getSessionlessConnection(): Promise<SessionlessConnectionData | null> {
309324
try {
310325
return (await this.getIDBItem<SessionlessConnectionData>(SESSIONLESS_CONNECTION_IDB_KEY)) ?? null
@@ -314,6 +329,15 @@ export class WebStorage implements SequenceStorage {
314329
}
315330
}
316331

332+
async getEthAuthProof(): Promise<ETHAuthProof | null> {
333+
try {
334+
return (await this.getIDBItem<ETHAuthProof>(ETH_AUTH_PROOF_IDB_KEY)) ?? null
335+
} catch (error) {
336+
console.error('Failed to retrieve ETHAuth proof:', error)
337+
return null
338+
}
339+
}
340+
317341
async clearSessionlessConnection(): Promise<void> {
318342
try {
319343
await this.deleteIDBItem(SESSIONLESS_CONNECTION_IDB_KEY)
@@ -323,6 +347,15 @@ export class WebStorage implements SequenceStorage {
323347
}
324348
}
325349

350+
async clearEthAuthProof(): Promise<void> {
351+
try {
352+
await this.deleteIDBItem(ETH_AUTH_PROOF_IDB_KEY)
353+
} catch (error) {
354+
console.error('Failed to clear ETHAuth proof:', error)
355+
throw error
356+
}
357+
}
358+
326359
async saveSessionlessConnectionSnapshot(sessionData: SessionlessConnectionData): Promise<void> {
327360
try {
328361
await this.setIDBItem(SESSIONLESS_CONNECTION_SNAPSHOT_IDB_KEY, sessionData)
@@ -363,6 +396,7 @@ export class WebStorage implements SequenceStorage {
363396
await this.clearExplicitSessions()
364397
await this.clearImplicitSession()
365398
await this.clearSessionlessConnection()
399+
await this.clearEthAuthProof()
366400
await this.clearSessionlessConnectionSnapshot()
367401
} catch (error) {
368402
console.error('Failed to clear all data:', error)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest'
2+
import { Secp256k1 } from 'ox'
3+
4+
import { ChainSessionManager } from '../src/ChainSessionManager.js'
5+
import { DappClient } from '../src/DappClient.js'
6+
import { TransportMode } from '../src/types/index.js'
7+
import { WebStorage } from '../src/utils/storage.js'
8+
9+
describe('ETHAuth proof persistence', () => {
10+
afterEach(() => {
11+
vi.unstubAllGlobals()
12+
})
13+
14+
it('persists ETHAuth proof only when requested during createNewSession', async () => {
15+
const fetchMock = vi.fn()
16+
vi.stubGlobal('fetch', fetchMock)
17+
vi.stubGlobal('window', { fetch: fetchMock } as any)
18+
19+
const ethAuthProof = {
20+
typedData: {
21+
domain: {},
22+
types: {},
23+
message: {},
24+
},
25+
ewtString: 'proof-string',
26+
} as any
27+
28+
const sequenceStorage = {
29+
setPendingRedirectRequest: vi.fn().mockResolvedValue(undefined),
30+
isRedirectRequestPending: vi.fn().mockResolvedValue(false),
31+
saveTempSessionPk: vi.fn().mockResolvedValue(undefined),
32+
getAndClearTempSessionPk: vi.fn().mockResolvedValue(null),
33+
savePendingRequest: vi.fn().mockResolvedValue(undefined),
34+
getAndClearPendingRequest: vi.fn().mockResolvedValue(null),
35+
peekPendingRequest: vi.fn().mockResolvedValue(null),
36+
saveExplicitSession: vi.fn().mockResolvedValue(undefined),
37+
getExplicitSessions: vi.fn().mockResolvedValue([]),
38+
clearExplicitSessions: vi.fn().mockResolvedValue(undefined),
39+
saveImplicitSession: vi.fn().mockResolvedValue(undefined),
40+
getImplicitSession: vi.fn().mockResolvedValue(null),
41+
clearImplicitSession: vi.fn().mockResolvedValue(undefined),
42+
saveSessionlessConnection: vi.fn().mockResolvedValue(undefined),
43+
getSessionlessConnection: vi.fn().mockResolvedValue(null),
44+
clearSessionlessConnection: vi.fn().mockResolvedValue(undefined),
45+
saveEthAuthProof: vi.fn().mockResolvedValue(undefined),
46+
getEthAuthProof: vi.fn().mockResolvedValue(ethAuthProof),
47+
clearEthAuthProof: vi.fn().mockResolvedValue(undefined),
48+
clearAllData: vi.fn().mockResolvedValue(undefined),
49+
} as any
50+
51+
const transport = {
52+
mode: TransportMode.POPUP,
53+
sendRequest: vi.fn().mockResolvedValue({
54+
walletAddress: '0x1111111111111111111111111111111111111111',
55+
ethAuthProof,
56+
}),
57+
closeWallet: vi.fn(),
58+
} as any
59+
60+
const manager = new ChainSessionManager(
61+
1,
62+
transport,
63+
'test-project-access-key',
64+
'https://keymachine.sequence.app',
65+
'https://nodes.sequence.app/{network}',
66+
'https://{network}-relayer.sequence.app',
67+
sequenceStorage,
68+
'https://example.com/redirect',
69+
undefined,
70+
vi.fn(() => Secp256k1.randomPrivateKey()),
71+
false,
72+
)
73+
74+
await manager.createNewSession('https://example.com', undefined, {
75+
ethAuth: {
76+
app: 'app-name',
77+
},
78+
})
79+
80+
expect(sequenceStorage.saveEthAuthProof).toHaveBeenCalledWith(ethAuthProof)
81+
expect(sequenceStorage.clearEthAuthProof).not.toHaveBeenCalled()
82+
})
83+
84+
it('clears ETHAuth proof on disconnect', async () => {
85+
const sequenceStorage = new WebStorage()
86+
const client = new DappClient('https://wallet.example', 'https://dapp.example', 'test-project-access-key', {
87+
sequenceStorage,
88+
})
89+
90+
const ethAuthProof = {
91+
typedData: {
92+
domain: {},
93+
types: {},
94+
message: {},
95+
},
96+
ewtString: 'proof-string',
97+
} as any
98+
99+
await sequenceStorage.saveEthAuthProof(ethAuthProof)
100+
expect(await client.getEthAuthProof()).toEqual(ethAuthProof)
101+
102+
await client.disconnect()
103+
104+
expect(await client.getEthAuthProof()).toBeNull()
105+
})
106+
})

0 commit comments

Comments
 (0)