Skip to content

Commit 70a0f6d

Browse files
authored
Merge pull request #5687 from WalletConnect/feat/session-properties
feat: scoped session properties
2 parents d79d860 + 8509bf9 commit 70a0f6d

File tree

10 files changed

+169
-17
lines changed

10 files changed

+169
-17
lines changed

packages/sign-client/src/controllers/engine.ts

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,14 @@ export class Engine extends IEngine {
189189
optionalNamespaces: params.optionalNamespaces || {},
190190
};
191191
await this.isValidConnect(connectParams);
192-
const { pairingTopic, requiredNamespaces, optionalNamespaces, sessionProperties, relays } =
193-
connectParams;
192+
const {
193+
pairingTopic,
194+
requiredNamespaces,
195+
optionalNamespaces,
196+
sessionProperties,
197+
scopedProperties,
198+
relays,
199+
} = connectParams;
194200
let topic = pairingTopic;
195201
let uri: string | undefined;
196202
let active = false;
@@ -232,6 +238,7 @@ export class Engine extends IEngine {
232238
expiryTimestamp,
233239
pairingTopic: topic,
234240
...(sessionProperties && { sessionProperties }),
241+
...(scopedProperties && { scopedProperties }),
235242
id: payloadId(),
236243
};
237244
const sessionConnectTarget = engineEvent("session_connect", proposal.id);
@@ -317,7 +324,8 @@ export class Engine extends IEngine {
317324
throw error;
318325
}
319326

320-
const { id, relayProtocol, namespaces, sessionProperties, sessionConfig } = params;
327+
const { id, relayProtocol, namespaces, sessionProperties, scopedProperties, sessionConfig } =
328+
params;
321329

322330
const proposal = this.client.proposal.get(id);
323331

@@ -353,6 +361,7 @@ export class Engine extends IEngine {
353361
controller: { publicKey: selfPublicKey, metadata: this.client.metadata },
354362
expiry: calcExpiry(SESSION_EXPIRY),
355363
...(sessionProperties && { sessionProperties }),
364+
...(scopedProperties && { scopedProperties }),
356365
...(sessionConfig && { sessionConfig }),
357366
};
358367
const transportType = TRANSPORT_TYPES.relay;
@@ -1919,8 +1928,15 @@ export class Engine extends IEngine {
19191928
const { id, params } = payload;
19201929
try {
19211930
this.isValidSessionSettleRequest(params);
1922-
const { relay, controller, expiry, namespaces, sessionProperties, sessionConfig } =
1923-
payload.params;
1931+
const {
1932+
relay,
1933+
controller,
1934+
expiry,
1935+
namespaces,
1936+
sessionProperties,
1937+
scopedProperties,
1938+
sessionConfig,
1939+
} = payload.params;
19241940
const pendingSession = [...this.pendingSessions.values()].find(
19251941
(s) => s.sessionTopic === topic,
19261942
);
@@ -1950,6 +1966,7 @@ export class Engine extends IEngine {
19501966
metadata: controller.metadata,
19511967
},
19521968
...(sessionProperties && { sessionProperties }),
1969+
...(scopedProperties && { scopedProperties }),
19531970
...(sessionConfig && { sessionConfig }),
19541971
transportType: TRANSPORT_TYPES.relay,
19551972
};
@@ -2454,11 +2471,13 @@ export class Engine extends IEngine {
24542471
payload: formatJsonRpcRequest(
24552472
"wc_sessionPropose",
24562473
{
2474+
...proposal,
24572475
requiredNamespaces: proposal.requiredNamespaces,
24582476
optionalNamespaces: proposal.optionalNamespaces,
24592477
relays: proposal.relays,
24602478
proposer: proposal.proposer,
24612479
sessionProperties: proposal.sessionProperties,
2480+
scopedProperties: proposal.scopedProperties,
24622481
},
24632482
proposal.id,
24642483
),
@@ -2570,8 +2589,14 @@ export class Engine extends IEngine {
25702589
);
25712590
throw new Error(message);
25722591
}
2573-
const { pairingTopic, requiredNamespaces, optionalNamespaces, sessionProperties, relays } =
2574-
params;
2592+
const {
2593+
pairingTopic,
2594+
requiredNamespaces,
2595+
optionalNamespaces,
2596+
sessionProperties,
2597+
scopedProperties,
2598+
relays,
2599+
} = params;
25752600
if (!isUndefined(pairingTopic)) await this.isValidPairingTopic(pairingTopic);
25762601

25772602
if (!isValidRelays(relays, true)) {
@@ -2593,6 +2618,24 @@ export class Engine extends IEngine {
25932618
if (!isUndefined(sessionProperties)) {
25942619
this.validateSessionProps(sessionProperties, "sessionProperties");
25952620
}
2621+
2622+
if (!isUndefined(scopedProperties)) {
2623+
this.validateSessionProps(scopedProperties, "scopedProperties");
2624+
2625+
const requestedNamespaces = Object.keys(requiredNamespaces || {}).concat(
2626+
Object.keys(optionalNamespaces || {}),
2627+
);
2628+
2629+
const scopedNamespaces = Object.keys(scopedProperties);
2630+
const valid = scopedNamespaces.every((ns) => requestedNamespaces.includes(ns));
2631+
if (!valid) {
2632+
throw new Error(
2633+
`Scoped properties must be a subset of required/optional namespaces, received: ${JSON.stringify(
2634+
scopedProperties,
2635+
)}, required/optional namespaces: ${JSON.stringify(requestedNamespaces)}`,
2636+
);
2637+
}
2638+
}
25962639
};
25972640

25982641
private validateNamespaces = (
@@ -2608,7 +2651,7 @@ export class Engine extends IEngine {
26082651
throw new Error(
26092652
getInternalError("MISSING_OR_INVALID", `approve() params: ${params}`).message,
26102653
);
2611-
const { id, namespaces, relayProtocol, sessionProperties } = params;
2654+
const { id, namespaces, relayProtocol, sessionProperties, scopedProperties } = params;
26122655

26132656
this.checkRecentlyDeleted(id);
26142657
await this.isValidProposalId(id);
@@ -2632,6 +2675,23 @@ export class Engine extends IEngine {
26322675
if (!isUndefined(sessionProperties)) {
26332676
this.validateSessionProps(sessionProperties, "sessionProperties");
26342677
}
2678+
2679+
if (!isUndefined(scopedProperties)) {
2680+
this.validateSessionProps(scopedProperties, "scopedProperties");
2681+
2682+
const approvedNamespaces = new Set(Object.keys(namespaces));
2683+
const scopedNamespaces = Object.keys(scopedProperties);
2684+
2685+
// the approved scoped namespaces must be a subset of the approved namespaces
2686+
const valid = scopedNamespaces.every((ns) => approvedNamespaces.has(ns));
2687+
if (!valid) {
2688+
throw new Error(
2689+
`Scoped properties must be a subset of approved namespaces, received: ${JSON.stringify(
2690+
scopedProperties,
2691+
)}, approved namespaces: ${Array.from(approvedNamespaces).join(", ")}`,
2692+
);
2693+
}
2694+
}
26352695
};
26362696

26372697
private isValidReject: EnginePrivate["isValidReject"] = async (params) => {
@@ -2889,12 +2949,14 @@ export class Engine extends IEngine {
28892949
return context;
28902950
};
28912951

2892-
private validateSessionProps = (properties: ProposalTypes.SessionProperties, type: string) => {
2893-
Object.values(properties).forEach((property) => {
2894-
if (!isValidString(property, false)) {
2952+
private validateSessionProps = (properties: SessionTypes.ScopedProperties, type: string) => {
2953+
Object.values(properties).forEach((property, index) => {
2954+
if (property === null || property === undefined) {
28952955
const { message } = getInternalError(
28962956
"MISSING_OR_INVALID",
2897-
`${type} must be in Record<string, string> format. Received: ${JSON.stringify(property)}`,
2957+
`${type} must contain an existing value for each key. Received: ${property} for key ${
2958+
Object.keys(properties)[index]
2959+
}`,
28982960
);
28992961
throw new Error(message);
29002962
}

packages/sign-client/test/sdk/client.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,46 @@ describe("Sign Client Integration", () => {
9494
expect(clients.B.metadata.redirect?.universal).to.exist;
9595
await deleteClients(clients);
9696
});
97+
it("should set scopedProperties in session", async () => {
98+
const clients = await initTwoClients();
99+
const requestedScopedProperties = {
100+
[Object.keys(TEST_CONNECT_PARAMS.requiredNamespaces)[0]]: "test",
101+
};
102+
const approvedScopedProperties = {
103+
polkadot: "approved",
104+
};
105+
const { uri, approval } = await clients.A.connect({
106+
...TEST_CONNECT_PARAMS,
107+
scopedProperties: requestedScopedProperties,
108+
});
109+
if (!uri) throw new Error("URI is undefined");
110+
111+
await Promise.all([
112+
new Promise<void>((resolve) => {
113+
clients.B.once("session_proposal", async (params) => {
114+
const { scopedProperties } = params.params;
115+
expect(scopedProperties).to.exist;
116+
expect(scopedProperties).to.deep.equal(requestedScopedProperties);
117+
118+
await clients.B.approve({
119+
id: params.id,
120+
namespaces: TEST_NAMESPACES,
121+
scopedProperties: approvedScopedProperties,
122+
});
123+
resolve();
124+
});
125+
}),
126+
clients.B.pair({ uri }),
127+
approval(),
128+
]);
129+
const dappSession = clients.A.session.getAll()[0];
130+
const walletSession = clients.B.session.getAll()[0];
131+
expect(dappSession.scopedProperties).to.deep.equal(approvedScopedProperties);
132+
expect(walletSession.scopedProperties).to.deep.equal(approvedScopedProperties);
133+
expect(dappSession.topic).to.eq(walletSession.topic);
134+
135+
await deleteClients(clients);
136+
});
97137
it("should connect with out of order URIs", async () => {
98138
const clients = await initTwoClients();
99139
// load three proposals

packages/sign-client/test/sdk/validation.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,38 @@ describe("Sign Client Validation", () => {
122122
expect(connect).toHaveProperty("uri");
123123
expect(connect.uri).to.be.string;
124124
});
125+
it("should reject connect with scopedProperties when not defined in requiredNamespaces or optionalNamespaces", async () => {
126+
await expect(
127+
clients.A.connect({
128+
scopedProperties: {
129+
"eip155:1": {},
130+
},
131+
}),
132+
).rejects.toThrowError(
133+
`Scoped properties must be a subset of required/optional namespaces, received: {"eip155:1":{}}, required/optional namespaces: []`,
134+
);
135+
});
125136
});
126137

127138
describe("approve", () => {
139+
it("should reject approve with scopedProperties when not defined in namespaces", async () => {
140+
const proposalId = await clients.A.connect(TEST_CONNECT_PARAMS).then(() => {
141+
return clients.A.proposal.keys[0];
142+
});
143+
await expect(
144+
clients.A.approve({
145+
id: proposalId,
146+
namespaces: TEST_APPROVE_PARAMS.namespaces,
147+
scopedProperties: {
148+
solana: "test",
149+
},
150+
}),
151+
).rejects.toThrowError(
152+
`Scoped properties must be a subset of approved namespaces, received: {"solana":"test"}, approved namespaces: ${Object.keys(
153+
TEST_APPROVE_PARAMS.namespaces,
154+
).join(", ")}`,
155+
);
156+
});
128157
it("throws when no params are passed", async () => {
129158
await expect(clients.A.approve()).rejects.toThrowError(
130159
"Missing or invalid. proposal id should be a number: undefined",

packages/types/src/sign-client/engine.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export declare namespace EngineTypes {
6161
requiredNamespaces?: ProposalTypes.RequiredNamespaces;
6262
optionalNamespaces?: ProposalTypes.OptionalNamespaces;
6363
sessionProperties?: ProposalTypes.SessionProperties;
64+
scopedProperties?: ProposalTypes.ScopedProperties;
6465
pairingTopic?: string;
6566
relays?: RelayerTypes.ProtocolOptions[];
6667
}
@@ -73,6 +74,7 @@ export declare namespace EngineTypes {
7374
id: number;
7475
namespaces: SessionTypes.Namespaces;
7576
sessionProperties?: ProposalTypes.SessionProperties;
77+
scopedProperties?: ProposalTypes.ScopedProperties;
7678
sessionConfig?: SessionTypes.SessionConfig;
7779
relayProtocol?: string;
7880
}

packages/types/src/sign-client/jsonrpc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export declare namespace JsonRpcTypes {
4343
relay: RelayerTypes.ProtocolOptions;
4444
namespaces: SessionTypes.Namespaces;
4545
sessionProperties?: ProposalTypes.SessionProperties;
46+
scopedProperties?: ProposalTypes.ScopedProperties;
4647
sessionConfig?: SessionTypes.SessionConfig;
4748
expiry: number;
4849
controller: {

packages/types/src/sign-client/proposal.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export declare namespace ProposalTypes {
1414
type RequiredNamespaces = Record<string, RequiredNamespace>;
1515
type OptionalNamespaces = Record<string, RequiredNamespace>;
1616
type SessionProperties = Record<string, string>;
17+
type ScopedProperties = Record<string, unknown>;
1718

1819
export interface Struct {
1920
id: number;
@@ -30,6 +31,7 @@ export declare namespace ProposalTypes {
3031
requiredNamespaces: RequiredNamespaces;
3132
optionalNamespaces: OptionalNamespaces;
3233
sessionProperties?: SessionProperties;
34+
scopedProperties?: ScopedProperties;
3335
pairingTopic: string;
3436
}
3537
}

packages/types/src/sign-client/session.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export declare namespace SessionTypes {
1818

1919
type Namespaces = Record<string, Namespace>;
2020

21+
type SessionProperties = ProposalTypes.SessionProperties;
22+
type ScopedProperties = ProposalTypes.ScopedProperties;
23+
2124
interface SessionConfig {
2225
disableDeepLink?: boolean;
2326
}
@@ -32,7 +35,8 @@ export declare namespace SessionTypes {
3235
namespaces: Namespaces;
3336
requiredNamespaces: ProposalTypes.RequiredNamespaces;
3437
optionalNamespaces: ProposalTypes.OptionalNamespaces;
35-
sessionProperties?: ProposalTypes.SessionProperties;
38+
sessionProperties?: SessionProperties;
39+
scopedProperties?: ScopedProperties;
3640
sessionConfig?: SessionConfig;
3741
self: {
3842
publicKey: string;

providers/ethereum-provider/src/EthereumProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface ConnectOps {
8282
optionalChains?: number[];
8383
rpcMap?: EthereumRpcMap;
8484
pairingTopic?: string;
85+
scopedProperties?: unknown;
8586
}
8687

8788
export type AuthenticateParams = {
@@ -297,6 +298,10 @@ export class EthereumProvider implements IEthereumProvider {
297298
}
298299
});
299300
}
301+
const scopedProperties = opts?.scopedProperties
302+
? { [this.namespace]: opts.scopedProperties }
303+
: undefined;
304+
300305
await this.signer
301306
.connect({
302307
namespaces: {
@@ -310,6 +315,7 @@ export class EthereumProvider implements IEthereumProvider {
310315
},
311316
}),
312317
pairingTopic: opts?.pairingTopic,
318+
scopedProperties,
313319
})
314320
.then((session?: SessionTypes.Struct) => {
315321
resolve(session);

0 commit comments

Comments
 (0)