Skip to content

Commit fe42c48

Browse files
committed
feat: support resumable client authentication
1 parent 30b6643 commit fe42c48

3 files changed

Lines changed: 87 additions & 28 deletions

File tree

packages/client/src/auth.test.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { fetchApiKeys } from './actions';
3+
import { createTestWalletClient, publicClient } from './testing';
4+
import { authenticateWith } from './viem';
5+
6+
const walletClient = createTestWalletClient();
7+
8+
describe('clients', () => {
9+
describe('PublicClient.beginAuthentication', () => {
10+
it('authenticates a secure client from an authentication workflow', async () => {
11+
const secureClient = await publicClient
12+
.beginAuthentication()
13+
.then(authenticateWith(walletClient));
14+
15+
await expect(fetchApiKeys(secureClient)).resolves.toBeDefined();
16+
});
17+
18+
it('authenticates with a non-zero nonce', async () => {
19+
const secureClient = await publicClient
20+
.beginAuthentication({ nonce: 1 })
21+
.then(authenticateWith(walletClient));
22+
23+
await expect(fetchApiKeys(secureClient)).resolves.toBeDefined();
24+
});
25+
26+
it('reuses stored credentials during authentication when they remain valid', async () => {
27+
const initialClient = await publicClient
28+
.beginAuthentication()
29+
.then(authenticateWith(walletClient));
30+
31+
const secureClient = await publicClient
32+
.beginAuthentication({ credentials: initialClient.credentials })
33+
.then(authenticateWith(walletClient));
34+
35+
await expect(fetchApiKeys(secureClient)).resolves.toBeDefined();
36+
});
37+
});
38+
});

packages/client/src/clients.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { ApiKeyCreds } from '@polymarket/bindings/clob';
22
import { expectEvmAddress, expectSignature } from '@polymarket/types';
3-
import { createOrDeriveApiKey } from './actions/auth';
3+
import { createOrDeriveApiKey, fetchApiKeys } from './actions/auth';
44
import {
55
type AuthenticationWorkflow,
66
createL2AuthTypedDataPayload,
77
} from './authentication';
88
import { type EnvironmentConfig, production } from './environments';
9+
import { RequestRejectedError } from './errors';
910
import { ServiceClient } from './ServiceClient';
1011

1112
export type PublicClientConfig = {
@@ -17,6 +18,10 @@ export type PublicClientConfig = {
1718
environment?: EnvironmentConfig;
1819
};
1920

21+
export type BeginAuthenticationOptions =
22+
| { credentials: ApiKeyCreds }
23+
| { nonce: number };
24+
2025
type Context = {
2126
/** @internal */
2227
environment: EnvironmentConfig;
@@ -29,6 +34,8 @@ type Context = {
2934
};
3035

3136
type SecureContext = Context & {
37+
/** @internal */
38+
address: string;
3239
/** @internal */
3340
credentials: ApiKeyCreds;
3441
};
@@ -66,40 +73,75 @@ class PublicClient extends AbstractClient<Context> {
6673
throw new Error('resume is not implemented yet');
6774
}
6875

69-
beginAuthentication(): Promise<AuthenticationWorkflow> {
76+
beginAuthentication(
77+
options?: BeginAuthenticationOptions,
78+
): Promise<AuthenticationWorkflow> {
7079
return Promise.resolve(
7180
async function* (this: PublicClient): AuthenticationWorkflow {
7281
const timestamp = Math.floor(Date.now() / 1000);
82+
const nonce =
83+
options !== undefined && 'nonce' in options ? options.nonce : 0;
7384
const address = expectEvmAddress(yield { kind: 'requestAddress' });
85+
86+
if (options !== undefined && 'credentials' in options) {
87+
const client = this.createSecureClient(options.credentials, address);
88+
89+
try {
90+
if (
91+
(await fetchApiKeys(client)).includes(options.credentials.key)
92+
) {
93+
return client;
94+
}
95+
} catch (error) {
96+
if (
97+
!(error instanceof RequestRejectedError) ||
98+
error.status !== 401
99+
) {
100+
throw error;
101+
}
102+
}
103+
}
104+
74105
const signature = yield {
75106
kind: 'signAuthMessage',
76107
payload: createL2AuthTypedDataPayload({
77108
address,
78109
chainId: this.environment.chainId,
110+
nonce,
79111
timestamp,
80112
}),
81113
};
82114
const credentials = await createOrDeriveApiKey(this, {
83115
address,
84-
nonce: 0,
116+
nonce,
85117
signature: expectSignature(signature),
86118
timestamp,
87119
});
88120

89-
return new SecureClient({
90-
...this.context,
91-
credentials,
92-
});
121+
return this.createSecureClient(credentials, address);
93122
}.call(this),
94123
);
95124
}
125+
126+
createSecureClient(credentials: ApiKeyCreds, address: string): SecureClient {
127+
return new SecureClient({
128+
...this.context,
129+
address,
130+
credentials,
131+
});
132+
}
96133
}
97134

98135
class SecureClient extends AbstractClient<SecureContext> {
99136
/** @internal */
100137
get credentials(): ApiKeyCreds {
101138
return this.context.credentials;
102139
}
140+
141+
/** @internal */
142+
get address(): string {
143+
return this.context.address;
144+
}
103145
}
104146

105147
export type { PublicClient, SecureClient };

0 commit comments

Comments
 (0)