diff --git a/.changeset/three-coats-rescue.md b/.changeset/three-coats-rescue.md new file mode 100644 index 0000000000000..219370429d617 --- /dev/null +++ b/.changeset/three-coats-rescue.md @@ -0,0 +1,5 @@ +--- +"@mysten/sui.js": minor +--- + +Add method to deserialize a public key, using it's schema and base64 data diff --git a/apps/wallet/src/background/keyring/Account.ts b/apps/wallet/src/background/keyring/Account.ts index 501a65a5a8deb..861c862bb7ccf 100644 --- a/apps/wallet/src/background/keyring/Account.ts +++ b/apps/wallet/src/background/keyring/Account.ts @@ -1,7 +1,12 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import type { Keypair, SuiAddress } from '@mysten/sui.js'; +import type { + SignaturePubkeyPair, + Keypair, + SuiAddress, + Base64DataBuffer, +} from '@mysten/sui.js'; export type AccountType = 'derived' | 'imported'; @@ -26,4 +31,12 @@ export class Account { exportKeypair() { return this.#keypair.export(); } + + async sign(data: Base64DataBuffer): Promise { + return { + signatureScheme: this.#keypair.getKeyScheme(), + signature: this.#keypair.signData(data), + pubKey: this.#keypair.getPublicKey(), + }; + } } diff --git a/apps/wallet/src/background/keyring/index.ts b/apps/wallet/src/background/keyring/index.ts index 520d4c673ca53..fd8a462764ddf 100644 --- a/apps/wallet/src/background/keyring/index.ts +++ b/apps/wallet/src/background/keyring/index.ts @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { Ed25519Keypair } from '@mysten/sui.js'; +import { Base64DataBuffer, Ed25519Keypair } from '@mysten/sui.js'; import mitt from 'mitt'; import { throttle } from 'throttle-debounce'; @@ -262,6 +262,36 @@ export class Keyring { await this.setLockTimeout(payload.args.timeout); } uiConnection.send(createMessage({ type: 'done' }, id)); + } else if (isKeyringPayload(payload, 'signData')) { + if (this.#locked) { + throw new Error('Keyring is locked. Unlock it first.'); + } + if (!payload.args) { + throw new Error('Missing parameters.'); + } + const { data, address } = payload.args; + const account = this.#accountsMap.get(address); + if (!account) { + throw new Error( + `Account for address ${address} not found in keyring` + ); + } + const { signature, signatureScheme, pubKey } = + await account.sign(new Base64DataBuffer(data)); + uiConnection.send( + createMessage>( + { + type: 'keyring', + method: 'signData', + return: { + signatureScheme, + signature: signature.toString(), + pubKey: pubKey.toBase64(), + }, + }, + id + ) + ); } } catch (e) { uiConnection.send( diff --git a/apps/wallet/src/shared/messaging/messages/payloads/keyring/index.ts b/apps/wallet/src/shared/messaging/messages/payloads/keyring/index.ts index 022b60513271c..3384cd11be286 100644 --- a/apps/wallet/src/shared/messaging/messages/payloads/keyring/index.ts +++ b/apps/wallet/src/shared/messaging/messages/payloads/keyring/index.ts @@ -3,7 +3,11 @@ import { isBasePayload } from '_payloads'; -import type { ExportedKeypair } from '@mysten/sui.js'; +import type { + ExportedKeypair, + SignatureScheme, + SuiAddress, +} from '@mysten/sui.js'; import type { BasePayload, Payload } from '_payloads'; type MethodToPayloads = { @@ -44,6 +48,14 @@ type MethodToPayloads = { args: { timeout: number }; return: never; }; + signData: { + args: { data: string; address: SuiAddress }; + return: { + signatureScheme: SignatureScheme; + signature: string; + pubKey: string; + }; + }; }; export interface KeyringPayload diff --git a/apps/wallet/src/ui/app/ApiProvider.ts b/apps/wallet/src/ui/app/ApiProvider.ts index 93bdf5c514af5..119c78680c897 100644 --- a/apps/wallet/src/ui/app/ApiProvider.ts +++ b/apps/wallet/src/ui/app/ApiProvider.ts @@ -1,17 +1,15 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { - RawSigner, - JsonRpcProvider, - LocalTxnDataSerializer, -} from '@mysten/sui.js'; +import { JsonRpcProvider, LocalTxnDataSerializer } from '@mysten/sui.js'; +import { BackgroundServiceSigner } from './background-client/BackgroundServiceSigner'; import { queryClient } from './helpers/queryClient'; import { growthbook } from '_app/experimentation/feature-gating'; import { FEATURES } from '_src/shared/experimentation/features'; -import type { Keypair } from '@mysten/sui.js'; +import type { BackgroundClient } from './background-client'; +import type { SuiAddress, SignerWithProvider } from '@mysten/sui.js'; export enum API_ENV { local = 'local', @@ -93,7 +91,7 @@ export const generateActiveNetworkList = (): NetworkTypes[] => { export default class ApiProvider { private _apiFullNodeProvider?: JsonRpcProvider; - private _signer: RawSigner | null = null; + private _signerByAddress: Map = new Map(); public setNewJsonRpcProvider( apiEnv: API_ENV = DEFAULT_API_ENV, @@ -105,7 +103,7 @@ export default class ApiProvider { customRPC ?? getDefaultAPI(apiEnv).fullNode, { faucetURL: customRPC ? '' : getDefaultAPI(apiEnv).faucet } ); - this._signer = null; + this._signerByAddress.clear(); } public get instance() { @@ -118,21 +116,28 @@ export default class ApiProvider { }; } - public getSignerInstance(keypair: Keypair): RawSigner { + public getSignerInstance( + address: SuiAddress, + backgroundClient: BackgroundClient + ): SignerWithProvider { if (!this._apiFullNodeProvider) { this.setNewJsonRpcProvider(); } - if (!this._signer) { - this._signer = new RawSigner( - keypair, - this._apiFullNodeProvider, - - growthbook.isOn(FEATURES.USE_LOCAL_TXN_SERIALIZER) - ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - new LocalTxnDataSerializer(this._apiFullNodeProvider!) - : undefined + if (!this._signerByAddress.has(address)) { + this._signerByAddress.set( + address, + new BackgroundServiceSigner( + address, + backgroundClient, + this._apiFullNodeProvider, + growthbook.isOn(FEATURES.USE_LOCAL_TXN_SERIALIZER) + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + new LocalTxnDataSerializer(this._apiFullNodeProvider!) + : undefined + ) ); } - return this._signer; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this._signerByAddress.get(address)!; } } diff --git a/apps/wallet/src/ui/app/background-client/BackgroundServiceSigner.ts b/apps/wallet/src/ui/app/background-client/BackgroundServiceSigner.ts new file mode 100644 index 0000000000000..2eb1cb4134bed --- /dev/null +++ b/apps/wallet/src/ui/app/background-client/BackgroundServiceSigner.ts @@ -0,0 +1,45 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { SignerWithProvider } from '@mysten/sui.js'; + +import type { BackgroundClient } from '.'; +import type { + Base64DataBuffer, + Provider, + SignaturePubkeyPair, + SuiAddress, + TxnDataSerializer, +} from '@mysten/sui.js'; + +export class BackgroundServiceSigner extends SignerWithProvider { + readonly #address: SuiAddress; + readonly #backgroundClient: BackgroundClient; + + constructor( + address: SuiAddress, + backgroundClient: BackgroundClient, + provider?: Provider, + serializer?: TxnDataSerializer + ) { + super(provider, serializer); + this.#address = address; + this.#backgroundClient = backgroundClient; + } + + async getAddress(): Promise { + return this.#address; + } + + signData(data: Base64DataBuffer): Promise { + return this.#backgroundClient.signData(this.#address, data); + } + + connect(provider: Provider): SignerWithProvider { + return new BackgroundServiceSigner( + this.#address, + this.#backgroundClient, + provider + ); + } +} diff --git a/apps/wallet/src/ui/app/background-client/index.ts b/apps/wallet/src/ui/app/background-client/index.ts index cbe176ff501a8..afd49a579e63c 100644 --- a/apps/wallet/src/ui/app/background-client/index.ts +++ b/apps/wallet/src/ui/app/background-client/index.ts @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import { Base64DataBuffer, publicKeyFromSerialized } from '@mysten/sui.js'; import { lastValueFrom, map, take } from 'rxjs'; import { growthbook } from '../experimentation/feature-gating'; @@ -17,7 +18,11 @@ import { setActiveOrigin } from '_redux/slices/app'; import { setPermissions } from '_redux/slices/permissions'; import { setTransactionRequests } from '_redux/slices/transaction-requests'; -import type { SuiAddress, SuiTransactionResponse } from '@mysten/sui.js'; +import type { + SignaturePubkeyPair, + SuiAddress, + SuiTransactionResponse, +} from '@mysten/sui.js'; import type { Message } from '_messages'; import type { KeyringPayload } from '_payloads/keyring'; import type { @@ -203,6 +208,43 @@ export class BackgroundClient { ); } + public async signData( + address: SuiAddress, + data: Base64DataBuffer + ): Promise { + return await lastValueFrom( + this.sendMessage( + createMessage>({ + type: 'keyring', + method: 'signData', + args: { data: data.toString(), address }, + }) + ).pipe( + take(1), + map(({ payload }) => { + if ( + isKeyringPayload(payload, 'signData') && + payload.return + ) { + const { signatureScheme, signature, pubKey } = + payload.return; + return { + signatureScheme, + signature: new Base64DataBuffer(signature), + pubKey: publicKeyFromSerialized( + signatureScheme, + pubKey + ), + }; + } + throw new Error( + 'Error unknown response for signData message' + ); + }) + ) + ); + } + private setupAppStatusUpdateInterval() { setInterval(() => { this.sendAppStatus(); diff --git a/apps/wallet/src/ui/app/hooks/useSigner.ts b/apps/wallet/src/ui/app/hooks/useSigner.ts index 0d5b2d2cbc9a6..38e2553bf9a9a 100644 --- a/apps/wallet/src/ui/app/hooks/useSigner.ts +++ b/apps/wallet/src/ui/app/hooks/useSigner.ts @@ -4,6 +4,9 @@ import { thunkExtras } from '_redux/store/thunk-extras'; export function useSigner() { - const { api, keypairVault } = thunkExtras; - return api.getSignerInstance(keypairVault.getKeypair()); + const { api, keypairVault, background } = thunkExtras; + return api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ); } diff --git a/apps/wallet/src/ui/app/redux/slices/sui-objects/Coin.ts b/apps/wallet/src/ui/app/redux/slices/sui-objects/Coin.ts index 9751da02688ed..7eb5fddf9119c 100644 --- a/apps/wallet/src/ui/app/redux/slices/sui-objects/Coin.ts +++ b/apps/wallet/src/ui/app/redux/slices/sui-objects/Coin.ts @@ -6,11 +6,11 @@ import { Coin as CoinAPI, SUI_TYPE_ARG } from '@mysten/sui.js'; import type { ObjectId, SuiObject, - RawSigner, SuiAddress, SuiMoveObject, JsonRpcProvider, SuiExecuteTransactionResponse, + SignerWithProvider, } from '@mysten/sui.js'; const COIN_TYPE = '0x2::coin::Coin'; @@ -85,7 +85,7 @@ export class Coin { * @param validator The sui address of the chosen validator */ public static async stakeCoin( - signer: RawSigner, + signer: SignerWithProvider, coins: SuiMoveObject[], amount: bigint, validator: SuiAddress @@ -107,7 +107,7 @@ export class Coin { } private static async requestSuiCoinWithExactAmount( - signer: RawSigner, + signer: SignerWithProvider, coins: SuiMoveObject[], amount: bigint ): Promise { @@ -143,7 +143,7 @@ export class Coin { } private static async selectSuiCoinWithExactAmount( - signer: RawSigner, + signer: SignerWithProvider, coins: SuiMoveObject[], amount: bigint, refreshData = false diff --git a/apps/wallet/src/ui/app/redux/slices/sui-objects/NFT.ts b/apps/wallet/src/ui/app/redux/slices/sui-objects/NFT.ts index 30c7a2a6382a9..7e34777534381 100644 --- a/apps/wallet/src/ui/app/redux/slices/sui-objects/NFT.ts +++ b/apps/wallet/src/ui/app/redux/slices/sui-objects/NFT.ts @@ -1,7 +1,10 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import type { RawSigner, SuiExecuteTransactionResponse } from '@mysten/sui.js'; +import type { + SignerWithProvider, + SuiExecuteTransactionResponse, +} from '@mysten/sui.js'; const DEFAULT_NFT_IMAGE = 'ipfs://QmZPWWy5Si54R3d26toaqRiqvCH7HkGdXkxwUgCm2oKKM2?filename=img-sq-01.png'; @@ -14,7 +17,7 @@ export class ExampleNFT { * @param signer A signer with connection to the fullnode */ public static async mintExampleNFT( - signer: RawSigner, + signer: SignerWithProvider, name?: string, description?: string, imageUrl?: string diff --git a/apps/wallet/src/ui/app/redux/slices/sui-objects/index.ts b/apps/wallet/src/ui/app/redux/slices/sui-objects/index.ts index f46f2feb1848b..a93959c88cce7 100644 --- a/apps/wallet/src/ui/app/redux/slices/sui-objects/index.ts +++ b/apps/wallet/src/ui/app/redux/slices/sui-objects/index.ts @@ -89,8 +89,11 @@ export const batchFetchObject = createAsyncThunk< export const mintDemoNFT = createAsyncThunk( 'mintDemoNFT', - async (_, { extra: { api, keypairVault }, dispatch }) => { - const signer = api.getSignerInstance(keypairVault.getKeypair()); + async (_, { extra: { api, keypairVault, background }, dispatch }) => { + const signer = api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ); await ExampleNFT.mintExampleNFT(signer); await dispatch(fetchAllOwnedAndRequiredObjects()); } @@ -107,18 +110,24 @@ export const transferNFT = createAsyncThunk< NFTTxResponse, { objectId: ObjectId; recipient: SuiAddress; gasBudget: number }, AppThunkConfig ->('transferNFT', async (data, { extra: { api, keypairVault }, dispatch }) => { - const signer = api.getSignerInstance(keypairVault.getKeypair()); - const txn = await signer.transferObject(data); - await dispatch(fetchAllOwnedAndRequiredObjects()); - const txnResp = { - timestamp_ms: getTimestampFromTransactionResponse(txn), - status: getExecutionStatusType(txn), - gasFee: txn ? getTotalGasUsed(txn) : 0, - txId: getTransactionDigest(txn), - }; - return txnResp as NFTTxResponse; -}); +>( + 'transferNFT', + async (data, { extra: { api, keypairVault, background }, dispatch }) => { + const signer = api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ); + const txn = await signer.transferObject(data); + await dispatch(fetchAllOwnedAndRequiredObjects()); + const txnResp = { + timestamp_ms: getTimestampFromTransactionResponse(txn), + status: getExecutionStatusType(txn), + gasFee: txn ? getTotalGasUsed(txn) : 0, + txId: getTransactionDigest(txn), + }; + return txnResp as NFTTxResponse; + } +); interface SuiObjectsManualState { loading: boolean; error: false | { code?: string; message?: string; name?: string }; diff --git a/apps/wallet/src/ui/app/redux/slices/transaction-requests/index.ts b/apps/wallet/src/ui/app/redux/slices/transaction-requests/index.ts index a5c902504afd2..3e0bcb374378c 100644 --- a/apps/wallet/src/ui/app/redux/slices/transaction-requests/index.ts +++ b/apps/wallet/src/ui/app/redux/slices/transaction-requests/index.ts @@ -74,9 +74,12 @@ export const deserializeTxn = createAsyncThunk< AppThunkConfig >( 'deserialize-transaction', - async (data, { dispatch, extra: { api, keypairVault } }) => { + async (data, { dispatch, extra: { api, keypairVault, background } }) => { const { id, serializedTxn } = data; - const signer = api.getSignerInstance(keypairVault.getKeypair()); + const signer = api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ); const localSerializer = new LocalTxnDataSerializer(signer.provider); const txnBytes = new Base64DataBuffer(serializedTxn); const version = await api.instance.fullNode.getRpcApiVersion(); @@ -143,7 +146,10 @@ export const respondToTransactionRequest = createAsyncThunk< let txResult: SuiTransactionResponse | undefined = undefined; let tsResultError: string | undefined; if (approved) { - const signer = api.getSignerInstance(keypairVault.getKeypair()); + const signer = api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ); try { let response: SuiExecuteTransactionResponse; if ( diff --git a/apps/wallet/src/ui/app/redux/slices/transactions/index.ts b/apps/wallet/src/ui/app/redux/slices/transactions/index.ts index 5286bb11a27ce..8f306fc3f4c14 100644 --- a/apps/wallet/src/ui/app/redux/slices/transactions/index.ts +++ b/apps/wallet/src/ui/app/redux/slices/transactions/index.ts @@ -39,11 +39,14 @@ export const sendTokens = createAsyncThunk< 'sui-objects/send-tokens', async ( { tokenTypeArg, amount, recipientAddress, gasBudget }, - { getState, extra: { api, keypairVault }, dispatch } + { getState, extra: { api, keypairVault, background }, dispatch } ) => { const state = getState(); const coins: SuiMoveObject[] = accountCoinsSelector(state); - const signer = api.getSignerInstance(keypairVault.getKeypair()); + const signer = api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ); const response = await signer.signAndExecuteTransaction( await CoinAPI.newPayTransaction( coins, @@ -73,7 +76,7 @@ export const stakeTokens = createAsyncThunk< 'sui-objects/stake', async ( { tokenTypeArg, amount, validatorAddress }, - { getState, extra: { api, keypairVault }, dispatch } + { getState, extra: { api, keypairVault, background }, dispatch } ) => { const state = getState(); const coinType = Coin.getCoinTypeFromArg(tokenTypeArg); @@ -88,7 +91,10 @@ export const stakeTokens = createAsyncThunk< .map(({ data }) => data as SuiMoveObject); const response = await Coin.stakeCoin( - api.getSignerInstance(keypairVault.getKeypair()), + api.getSignerInstance( + keypairVault.getKeypair().getPublicKey().toSuiAddress(), + background + ), coins, amount, validatorAddress diff --git a/sdk/typescript/src/cryptography/publickey.ts b/sdk/typescript/src/cryptography/publickey.ts index 9b2dea002be3f..119e53c209686 100644 --- a/sdk/typescript/src/cryptography/publickey.ts +++ b/sdk/typescript/src/cryptography/publickey.ts @@ -1,6 +1,9 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import { Ed25519PublicKey } from './ed25519-publickey'; +import { Secp256k1PublicKey } from './secp256k1-publickey'; + /** * Value to be converted into public key. */ @@ -60,3 +63,16 @@ export interface PublicKey { */ toSuiAddress(): string; } + +export function publicKeyFromSerialized( + schema: SignatureScheme, + pubKey: string +): PublicKey { + if (schema === 'ED25519') { + return new Ed25519PublicKey(pubKey); + } + if (schema === 'Secp256k1') { + return new Secp256k1PublicKey(pubKey); + } + throw new Error('Unknown public key schema'); +}