Skip to content

Commit

Permalink
[wallet-standard] Introduce wallet-standard package for wallet develo…
Browse files Browse the repository at this point in the history
…pers (MystenLabs#4925)
  • Loading branch information
Jordan-Mysten authored Oct 11, 2022
1 parent 852b937 commit 5ac98bc
Show file tree
Hide file tree
Showing 51 changed files with 1,408 additions and 559 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-beans-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/wallet-adapter-base": minor
---

Add new icon property to the base adapter. Introduce a WalletAdapterProvider API, which can be used to dynamically provide multiple wallet adapters.
5 changes: 5 additions & 0 deletions .changeset/cool-chicken-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/wallet-adapter-sui-wallet": minor
---

Mark wallet-adapter-sui-wallet as legacy, in favor of the standardized wallet adapter.
12 changes: 12 additions & 0 deletions .changeset/cool-coins-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@mysten/wallet-adapter-all-wallets": minor
"@mysten/wallet-adapter-base": minor
"@mysten/wallet-adapter-mock-wallet": minor
"@mysten/wallet-adapter-sui-wallet": minor
"@mysten/wallet-adapter-wallet-standard": minor
"@mysten/wallet-adapter-react": minor
"@mysten/wallet-adapter-react-ui": minor
"@mysten/wallet-standard": minor
---

Introduce new wallet adapter based on the Wallet Standard. This wallet adapter automatically detects wallets that adhere to the standard interface.
5 changes: 5 additions & 0 deletions .changeset/loud-scissors-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/wallet-standard": minor
---

Introduce new "wallet-standard" package which can be used to build wallets that are compatible with the Wallet Standard.
5 changes: 5 additions & 0 deletions .changeset/ninety-shoes-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mysten/wallet-adapter-all-wallets": patch
---

Add support for standard wallet adapter.
5 changes: 4 additions & 1 deletion apps/wallet/configs/ts/tsconfig.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@
"_utils/*": ["./src/ui/styles/utils/*"],
"_font-icons/*": ["./font-icons/*"],
"@mysten/sui.js": ["../../sdk/typescript/src/"],
"@mysten/bcs": ["../../sdk/bcs/src/"]
"@mysten/bcs": ["../../sdk/bcs/src/"],
"@mysten/wallet-standard": [
"../../sdk/wallet-adapter/packages/wallet-standard/src/"
]
}
},
"include": ["../../src"],
Expand Down
1 change: 1 addition & 0 deletions apps/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@growthbook/growthbook": "^0.18.1",
"@metamask/browser-passworder": "^3.0.0",
"@mysten/sui.js": "workspace:*",
"@mysten/wallet-standard": "workspace:*",
"@reduxjs/toolkit": "^1.8.3",
"@scure/bip32": "^1.1.0",
"@scure/bip39": "^1.1.0",
Expand Down
22 changes: 2 additions & 20 deletions apps/wallet/src/dapp-interface/DAppInterface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { filter, lastValueFrom, map, take } from 'rxjs';
import { filter, map } from 'rxjs';

import { mapToPromise } from './utils';
import { createMessage } from '_messages';
import { WindowMessageStream } from '_messaging/WindowMessageStream';
import { isErrorPayload } from '_payloads';
import { ALL_PERMISSION_TYPES } from '_payloads/permissions';

import type {
Expand All @@ -29,24 +29,6 @@ import type {
} from '_payloads/transactions';
import type { Observable } from 'rxjs';

function mapToPromise<T extends Payload, R>(
stream: Observable<T>,
project: (value: T) => R
) {
return lastValueFrom(
stream.pipe(
take<T>(1),
map<T, R>((response) => {
if (isErrorPayload(response)) {
// TODO: throw proper error
throw new Error(response.message);
}
return project(response);
})
)
);
}

export class DAppInterface {
private _messagesStream: WindowMessageStream;

Expand Down
183 changes: 183 additions & 0 deletions apps/wallet/src/dapp-interface/WalletStandardInterface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import {
SUI_CHAINS,
ReadonlyWalletAccount,
type SuiSignAndExecuteTransactionFeature,
type SuiSignAndExecuteTransactionMethod,
type ConnectFeature,
type ConnectMethod,
type Wallet,
type EventsFeature,
type EventsOnMethod,
type EventsListeners,
} from '@mysten/wallet-standard';
import mitt, { type Emitter } from 'mitt';
import { filter, map, type Observable } from 'rxjs';

import icon from '../manifest/icons/sui-icon-128.png';
import { mapToPromise } from './utils';
import { createMessage } from '_messages';
import { WindowMessageStream } from '_messaging/WindowMessageStream';
import { type Payload } from '_payloads';
import {
type AcquirePermissionsRequest,
type AcquirePermissionsResponse,
ALL_PERMISSION_TYPES,
} from '_payloads/permissions';

import type { GetAccount } from '_payloads/account/GetAccount';
import type { GetAccountResponse } from '_payloads/account/GetAccountResponse';
import type {
ExecuteTransactionRequest,
ExecuteTransactionResponse,
} from '_payloads/transactions';

type WalletEventsMap = {
[E in keyof EventsListeners]: Parameters<EventsListeners[E]>[0];
};

// TODO: rebuild event interface with Mitt.
export class SuiWallet implements Wallet {
readonly #events: Emitter<WalletEventsMap>;
readonly #version = '1.0.0' as const;
readonly #name = 'Sui Wallet' as const;
#account: ReadonlyWalletAccount | null;
#messagesStream: WindowMessageStream;

get version() {
return this.#version;
}

get name() {
return this.#name;
}

get icon() {
// TODO: Improve this with ideally a vector logo.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return icon as any;
}

get chains() {
// TODO: Extract chain from wallet:
return SUI_CHAINS;
}

get features(): ConnectFeature &
EventsFeature &
SuiSignAndExecuteTransactionFeature {
return {
'standard:connect': {
version: '1.0.0',
connect: this.#connect,
},
'standard:events': {
version: '1.0.0',
on: this.#on,
},
'sui:signAndExecuteTransaction': {
version: '1.0.0',
signAndExecuteTransaction: this.#signAndExecuteTransaction,
},
};
}

get accounts() {
return this.#account ? [this.#account] : [];
}

constructor() {
this.#events = mitt();
this.#account = null;
this.#messagesStream = new WindowMessageStream(
'sui_in-page',
'sui_content-script'
);

this.#connected();
}

#on: EventsOnMethod = (event, listener) => {
this.#events.on(event, listener);
return () => this.#events.off(event, listener);
};

#connected = async () => {
const accounts = await mapToPromise(
this.#send<GetAccount, GetAccountResponse>({
type: 'get-account',
}),
(response) => response.accounts
);

const [address] = accounts;

if (address) {
const account = this.#account;
if (!account || account.address !== address) {
this.#account = new ReadonlyWalletAccount({
address,
// TODO: Expose public key instead of address:
publicKey: new Uint8Array(),
chains: SUI_CHAINS,
features: [
'sui:signAndExecuteTransaction',
'standard:signMessage',
],
});
this.#events.emit('change', { accounts: this.accounts });
}
}
};

#connect: ConnectMethod = async (input) => {
if (!input?.silent) {
await mapToPromise(
this.#send<
AcquirePermissionsRequest,
AcquirePermissionsResponse
>({
type: 'acquire-permissions-request',
permissions: ALL_PERMISSION_TYPES,
}),
(response) => response.result
);
}

await this.#connected();

return { accounts: this.accounts };
};

#signAndExecuteTransaction: SuiSignAndExecuteTransactionMethod = async (
input
) => {
return mapToPromise(
this.#send<ExecuteTransactionRequest, ExecuteTransactionResponse>({
type: 'execute-transaction-request',
transaction: {
type: 'v2',
data: input.transaction,
},
}),
(response) => response.result
);
};

#send<
RequestPayload extends Payload,
ResponsePayload extends Payload | void = void
>(
payload: RequestPayload,
responseForID?: string
): Observable<ResponsePayload> {
const msg = createMessage(payload, responseForID);
this.#messagesStream.send(msg);
return this.#messagesStream.messages.pipe(
filter(({ id }) => id === msg.id),
map((msg) => msg.payload as ResponsePayload)
);
}
}
10 changes: 10 additions & 0 deletions apps/wallet/src/dapp-interface/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
// SPDX-License-Identifier: Apache-2.0

import { DAppInterface } from './DAppInterface';
import { SuiWallet } from './WalletStandardInterface';

import type { WalletsWindow } from '@mysten/wallet-standard';

declare const window: WalletsWindow;

window.navigator.wallets = window.navigator.wallets || [];
window.navigator.wallets.push(({ register }) => {
register(new SuiWallet());
});

Object.defineProperty(window, 'suiWallet', {
enumerable: false,
Expand Down
24 changes: 24 additions & 0 deletions apps/wallet/src/dapp-interface/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { lastValueFrom, map, take, type Observable } from 'rxjs';

import { isErrorPayload, type Payload } from '_payloads';

export function mapToPromise<T extends Payload, R>(
stream: Observable<T>,
project: (value: T) => R
) {
return lastValueFrom(
stream.pipe(
take<T>(1),
map<T, R>((response) => {
if (isErrorPayload(response)) {
// TODO: throw proper error
throw new Error(response.message);
}
return project(response);
})
)
);
}
Loading

0 comments on commit 5ac98bc

Please sign in to comment.