Skip to content

Commit

Permalink
Add ZKSend wallet implementation and SDK for creating ZKSend links (M…
Browse files Browse the repository at this point in the history
…ystenLabs#15584)

## Description 

Describe the changes or additions included in this PR.

## Test Plan 

How did you test the new or updated feature?

---
If your changes are not user-facing and not a breaking change, you can
skip the following section. Otherwise, please indicate what changed, and
then add to the Release Notes section as highlighted during the release
process.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes

---------

Co-authored-by: Michael Hayes <michael.hayes@mystenlabs.com>
  • Loading branch information
Jordan-Mysten and hayes-mysten authored Jan 10, 2024
1 parent 0c0a906 commit e81f49e
Show file tree
Hide file tree
Showing 10 changed files with 812 additions and 20 deletions.
41 changes: 22 additions & 19 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "minor",
"privatePackages": false,
"ignore": [
"sui-wallet",
"sui-explorer",
"@mysten/core",
"@mysten/ui",
"sponsored-transactions",
"kiosk-demo",
"kiosk-cli",
"@mysten/sdk-docs"
]
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "minor",
"privatePackages": false,
"ignore": [
"sui-wallet",
"sui-explorer",
"@mysten/core",
"@mysten/ui",
"sponsored-transactions",
"kiosk-demo",
"kiosk-cli",
"@mysten/sdk-docs"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
}
}
5 changes: 5 additions & 0 deletions .changeset/fluffy-turtles-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mysten/zksend': minor
---

Add SDK for creating ZKSend links
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion sdk/zksend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
"vitest": "^0.33.0"
},
"dependencies": {
"@mysten/wallet-standard": "workspace:*"
"@mysten/sui.js": "workspace:*",
"@mysten/wallet-standard": "workspace:*",
"mitt": "^3.0.1",
"nanostores": "^0.9.3",
"valibot": "^0.25.0"
}
}
88 changes: 88 additions & 0 deletions sdk/zksend/src/channel/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import type { Output } from 'valibot';
import { literal, object, string, union, uuid } from 'valibot';

export type ZkSendSignPersonalMessageResponse = Output<typeof ZkSendSignPersonalMessageResponse>;

export const ZkSendRejectResponse = object({
type: literal('reject'),
});

export const ZdSendConnectResponse = object({
address: string(),
});

export const ZkSendSignTransactionBlockResponse = object({
signature: string(),
});

export const ZkSendSignPersonalMessageResponse = object({
signature: string(),
});

export const ZkSendRequestType = union([
literal('connect'),
literal('sign-transaction-block'),
literal('sign-personal-message'),
]);

export const ZkSendConnectRequest = object({});
export const ZkSendSignTransactionBlockRequest = object({
bytes: string(),
address: string(),
});
export const ZkSendSignPersonalMessageRequest = object({
bytes: string(),
address: string(),
});
export const ZkSendRequestData = union([
ZkSendConnectRequest,
ZkSendSignTransactionBlockRequest,
ZkSendSignPersonalMessageRequest,
]);

export const ZkSendRequest = object({
id: string([uuid()]),
origin: string(),
name: string(),
type: ZkSendRequestType,
data: ZkSendRequestData,
});
export interface ZkSendRequestTypes extends Record<string, Record<string, string>> {
// eslint-disable-next-line @typescript-eslint/ban-types
connect: Output<typeof ZkSendConnectRequest>;
'sign-transaction-block': Output<typeof ZkSendSignTransactionBlockRequest>;
'sign-personal-message': Output<typeof ZkSendSignPersonalMessageRequest>;
}

export type ZkSendResponseTypes = {
connect: Output<typeof ZdSendConnectResponse>;
'sign-transaction-block': Output<typeof ZkSendSignTransactionBlockResponse>;
'sign-personal-message': Output<typeof ZkSendSignPersonalMessageResponse>;
};

export const ZkSendResponseData = union([
ZdSendConnectResponse,
ZkSendSignTransactionBlockResponse,
ZkSendSignPersonalMessageResponse,
]);

export const ZkSendResolveResponse = object({
type: literal('resolve'),
data: ZkSendResponseData,
});

export type ZkSendResolveResponse = Output<typeof ZkSendResolveResponse>;

export const ZkSendResponsePayload = union([ZkSendRejectResponse, ZkSendResolveResponse]);
export type ZkSendResponsePayload = Output<typeof ZkSendResponsePayload>;

export const ZkSendResponse = object({
id: string([uuid()]),
source: literal('zksend-channel'),
payload: ZkSendResponsePayload,
});

export type ZkSendResponse = Output<typeof ZkSendResponse>;
125 changes: 125 additions & 0 deletions sdk/zksend/src/channel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import type { Output } from 'valibot';
import { safeParse } from 'valibot';

import { withResolvers } from '../utils/withResolvers.js';
import type { ZkSendRequestTypes, ZkSendResponsePayload, ZkSendResponseTypes } from './events.js';
import { ZkSendRequest, ZkSendResponse } from './events.js';

const DEFAULT_ZKSEND_ORIGIN = 'https://zksend.com';

interface ZkSendPopupOptions {
origin?: string;
name: string;
}

export class ZkSendPopup {
#id: string;
#origin: string;
#name: string;

#close?: () => void;

constructor({ origin = DEFAULT_ZKSEND_ORIGIN, name }: ZkSendPopupOptions) {
this.#id = crypto.randomUUID();
this.#origin = origin;
this.#name = name;
}

async createRequest<T extends keyof ZkSendResponseTypes>(
type: T,
data: ZkSendRequestTypes[T],
): Promise<ZkSendResponseTypes[T]> {
const { promise, resolve, reject } = withResolvers<ZkSendResponseTypes[T]>();

let popup: Window | null = null;

const listener = (event: MessageEvent) => {
if (event.origin !== this.#origin) {
return;
}
const { success, output } = safeParse(ZkSendResponse, event.data);
if (!success || output.id !== this.#id) return;

window.removeEventListener('message', listener);

if (output.payload.type === 'reject') {
reject(new Error('TODO: Better error message'));
} else if (output.payload.type === 'resolve') {
resolve(output.payload.data as ZkSendResponseTypes[T]);
}
};

this.#close = () => {
popup?.close();
window.removeEventListener('message', listener);
};

window.addEventListener('message', listener);

popup = window.open(
`${this.#origin}/dapp/${type}?${new URLSearchParams({
id: this.#id,
origin: window.origin,
name: this.#name,
})}${data ? `#${new URLSearchParams(data)}` : ''}`,
);

if (!popup) {
throw new Error('TODO: Better error message');
}

return promise;
}

close() {
this.#close?.();
}
}

export class ZkSendHost {
#request: Output<typeof ZkSendRequest>;

constructor(request: Output<typeof ZkSendRequest>) {
if (typeof window === 'undefined' || !window.opener) {
throw new Error('TODO: Better error message');
}

this.#request = request;
}

static fromUrl(url: string = window.location.href) {
const parsed = new URL(url);

const request = safeParse(ZkSendRequest, {
id: parsed.searchParams.get('id'),
origin: parsed.searchParams.get('origin'),
name: parsed.searchParams.get('name'),
type: parsed.pathname.split('/').pop(),
data: parsed.hash ? Object.fromEntries(new URLSearchParams(parsed.hash.slice(1))) : {},
});

if (request.issues) {
throw new Error('TODO: Better error message');
}

return new ZkSendHost(request.output);
}

getRequestData() {
return this.#request;
}

sendMessage(payload: ZkSendResponsePayload) {
window.opener.postMessage(
{
id: this.#request.id,
source: 'zksend-channel',
payload,
} satisfies ZkSendResponse,
this.#request.origin,
);
}
}
4 changes: 4 additions & 0 deletions sdk/zksend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

export * from './links.js';
export * from './wallet.js';
export * from './channel/index.js';
Loading

0 comments on commit e81f49e

Please sign in to comment.