Skip to content

Commit

Permalink
Merge pull request #161 from xmtp/rygine/multi-wallet-consent-support
Browse files Browse the repository at this point in the history
Add support for multiple wallets to consent
  • Loading branch information
rygine authored Jan 25, 2024
2 parents 2a4fdda + 0946dfb commit 388f149
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 64 deletions.
7 changes: 7 additions & 0 deletions .changeset/forty-otters-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@xmtp/react-sdk": major
---

Add support for multiple wallets to consent

Since this changes the API of several exports, it's a breaking change that requires a major release.
58 changes: 44 additions & 14 deletions packages/react-sdk/src/helpers/caching/consent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,41 @@ describe("Consent helpers", () => {
it("should add a new entry and update it", async () => {
const undefinedEntry = await getCachedConsentState(
testWallet1.account.address,
testWallet2.account.address,
db,
);
expect(undefinedEntry).toBeUndefined();
await putConsentState(testWallet1.account.address, "allowed", db);
await putConsentState(
testWallet1.account.address,
testWallet2.account.address,
"allowed",
db,
);
const entry = await getCachedConsentState(
testWallet1.account.address,
testWallet2.account.address,
db,
);
expect(entry).toEqual({
peerAddress: testWallet1.account.address,
peerAddress: testWallet2.account.address,
state: "allowed",
walletAddress: testWallet1.account.address,
});
await putConsentState(testWallet1.account.address, "denied", db);
await putConsentState(
testWallet1.account.address,
testWallet2.account.address,
"denied",
db,
);
const updatedEntry = await getCachedConsentState(
testWallet1.account.address,
testWallet2.account.address,
db,
);
expect(updatedEntry).toEqual({
peerAddress: testWallet1.account.address,
peerAddress: testWallet2.account.address,
state: "denied",
walletAddress: testWallet1.account.address,
});
});
});
Expand All @@ -53,31 +68,46 @@ describe("Consent helpers", () => {
await bulkPutConsentState(
[
{
peerAddress: testWallet1.account.address,
peerAddress: testWallet2.account.address,
state: "allowed",
walletAddress: testWallet1.account.address,
},
{
peerAddress: testWallet2.account.address,
peerAddress: testWallet1.account.address,
state: "denied",
walletAddress: testWallet2.account.address,
},
],
db,
);
const entries = await getCachedConsentEntries(db);

entries.forEach((entry) => {
expect(entry.entryType).toBe("address");
expect(entry.permissionType).toBe(
entry.value === testWallet1.account.address ? "allowed" : "denied",
);
});
const entries = await getCachedConsentEntries(
testWallet1.account.address,
db,
);
expect(entries[0].entryType).toBe("address");
expect(entries[0].value).toBe(testWallet2.account.address);
expect(entries[0].permissionType).toBe("allowed");

const entries2 = await getCachedConsentEntries(
testWallet2.account.address,
db,
);
expect(entries2[0].entryType).toBe("address");
expect(entries2[0].value).toBe(testWallet1.account.address);
expect(entries2[0].permissionType).toBe("denied");
});
});

describe("loadConsentListFromCache", () => {
it("should load consent list entries from the cache", async () => {
const testClient = await Client.create(testWallet1, { env: "local" });
await putConsentState(testWallet2.account.address, "denied", db);
await putConsentState(
testWallet1.account.address,
testWallet2.account.address,
"denied",
db,
);
await loadConsentListFromCache(testClient, db);
expect(testClient.contacts.isDenied(testWallet2.account.address)).toBe(
true,
Expand Down
30 changes: 23 additions & 7 deletions packages/react-sdk/src/helpers/caching/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,43 @@ import { ConsentListEntry } from "@xmtp/xmtp-js";
export type CachedConsentEntry = {
peerAddress: string;
state: ConsentState;
walletAddress: string;
};

export type CachedConsentTable = Table<CachedConsentEntry, string>;

/**
* Retrieve a cached consent state by peer address
* Retrieve a cached consent state by wallet and peer address
*
* @returns The cached consent state if found, otherwise `undefined`
*/
export const getCachedConsentState = async (peerAddress: string, db: Dexie) => {
export const getCachedConsentState = async (
walletAddress: string,
peerAddress: string,
db: Dexie,
) => {
const consentTable = db.table("consent") as CachedConsentTable;
return consentTable.where("peerAddress").equals(peerAddress).first();
return consentTable
.where({
walletAddress,
peerAddress,
})
.first();
};

/**
* Retrieve all cached consent entries
*
* @returns An array of ConsentListEntry instances
*/
export const getCachedConsentEntries = async (db: Dexie) => {
export const getCachedConsentEntries = async (
walletAddress: string,
db: Dexie,
) => {
const consentTable = db.table("consent") as CachedConsentTable;
return (await consentTable.toArray()).map((entry) =>
const entries = await consentTable.where({ walletAddress }).toArray();

return entries.map((entry) =>
ConsentListEntry.fromAddress(entry.peerAddress, entry.state),
);
};
Expand All @@ -37,7 +52,7 @@ export const getCachedConsentEntries = async (db: Dexie) => {
* Load the cached consent list entries into the XMTP client
*/
export const loadConsentListFromCache = async (client: Client, db: Dexie) => {
const cachedEntries = await getCachedConsentEntries(db);
const cachedEntries = await getCachedConsentEntries(client.address, db);
client.contacts.setConsentListEntries(cachedEntries);
};

Expand All @@ -48,13 +63,14 @@ const putConsentStateMutex = new Mutex();
* Add or update a peer address's consent state
*/
export const putConsentState = async (
walletAddress: string,
peerAddress: string,
state: ConsentState,
db: Dexie,
) =>
putConsentStateMutex.runExclusive(async () => {
const consentTable = db.table("consent") as CachedConsentTable;
await consentTable.put({ peerAddress, state });
await consentTable.put({ peerAddress, state, walletAddress });
});

/**
Expand Down
4 changes: 3 additions & 1 deletion packages/react-sdk/src/helpers/caching/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ export const getDbInstance = (options?: GetDBInstanceOptions) => {
xmtpID
`,
consent: `
[walletAddress+peerAddress],
peerAddress,
state
state,
walletAddress
`,
});
}
Expand Down
14 changes: 12 additions & 2 deletions packages/react-sdk/src/hooks/useConsent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ describe("useConsent", () => {
await result.current.allow([testWallet2.account.address]);
expect(allowSpy).toHaveBeenCalledWith([testWallet2.account.address]);
const entry = await getCachedConsentState(
testWallet1.account.address,
testWallet2.account.address,
db,
);
expect(entry).toEqual({
peerAddress: testWallet2.account.address,
state: "allowed",
walletAddress: testWallet1.account.address,
});
});
});
Expand All @@ -74,12 +76,14 @@ describe("useConsent", () => {
await result.current.deny([testWallet2.account.address]);
expect(allowSpy).toHaveBeenCalledWith([testWallet2.account.address]);
const entry = await getCachedConsentState(
testWallet1.account.address,
testWallet2.account.address,
db,
);
expect(entry).toEqual({
peerAddress: testWallet2.account.address,
state: "denied",
walletAddress: testWallet1.account.address,
});
});
});
Expand All @@ -100,7 +104,10 @@ describe("useConsent", () => {
expect(list2[0].entryType).toEqual("address");
expect(list2[0].permissionType).toEqual("allowed");
expect(list2[0].value).toEqual(testWallet2.account.address);
const entries = await getCachedConsentEntries(db);
const entries = await getCachedConsentEntries(
testWallet1.account.address,
db,
);
expect(entries.length).toEqual(1);
expect(entries[0].entryType).toEqual("address");
expect(entries[0].permissionType).toEqual("allowed");
Expand All @@ -124,7 +131,10 @@ describe("useConsent", () => {
expect(list2[0].entryType).toEqual("address");
expect(list2[0].permissionType).toEqual("allowed");
expect(list2[0].value).toEqual(testWallet4.account.address);
const entries = await getCachedConsentEntries(db);
const entries = await getCachedConsentEntries(
testWallet3.account.address,
db,
);
expect(entries.length).toEqual(1);
expect(entries[0].entryType).toEqual("address");
expect(entries[0].permissionType).toEqual("allowed");
Expand Down
94 changes: 54 additions & 40 deletions packages/react-sdk/src/hooks/useConsent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,68 +12,82 @@ export const useConsent = () => {

const allow = useCallback(
async (addresses: string[]) => {
await client?.contacts.allow(addresses);
// update DB
await bulkPutConsentState(
addresses.map((peerAddress) => ({
peerAddress,
state: "allowed",
})),
db,
);
if (client?.address) {
await client?.contacts.allow(addresses);
// update DB
await bulkPutConsentState(
addresses.map((peerAddress) => ({
peerAddress,
state: "allowed",
walletAddress: client.address,
})),
db,
);
}
},
[client?.contacts, db],
[client?.address, client?.contacts, db],
);

const deny = useCallback(
async (addresses: string[]) => {
await client?.contacts.deny(addresses);
// update DB
await bulkPutConsentState(
addresses.map((peerAddress) => ({
peerAddress,
state: "denied",
})),
db,
);
if (client?.address) {
await client?.contacts.deny(addresses);
// update DB
await bulkPutConsentState(
addresses.map((peerAddress) => ({
peerAddress,
state: "denied",
walletAddress: client.address,
})),
db,
);
}
},
[client?.contacts, db],
[client?.address, client?.contacts, db],
);

const loadConsentList = useCallback(
async (startTime?: Date) => {
const entries = await client?.contacts.loadConsentList(startTime);
if (client?.address) {
const entries = await client?.contacts.loadConsentList(startTime);
if (entries) {
// update DB
await bulkPutConsentState(
entries.map((entry) => ({
peerAddress: entry.value,
state: entry.permissionType,
walletAddress: client.address,
})),
db,
);
}
return entries ?? [];
}
return [];
},
[client?.address, client?.contacts, db],
);

const refreshConsentList = useCallback(async () => {
if (client?.address) {
// clear consent DB table
await db.table("consent").clear();
const entries = await client?.contacts.refreshConsentList();
if (entries) {
// update DB
await bulkPutConsentState(
entries.map((entry) => ({
peerAddress: entry.value,
state: entry.permissionType,
walletAddress: client.address,
})),
db,
);
}
return entries ?? [];
},
[client?.contacts, db],
);

const refreshConsentList = useCallback(async () => {
// clear consent DB table
await db.table("consent").clear();
const entries = await client?.contacts.refreshConsentList();
if (entries) {
// update DB
await bulkPutConsentState(
entries.map((entry) => ({
peerAddress: entry.value,
state: entry.permissionType,
})),
db,
);
}
return entries ?? [];
}, [client?.contacts, db]);
return [];
}, [client?.address, client?.contacts, db]);

return {
allow,
Expand Down

0 comments on commit 388f149

Please sign in to comment.