Skip to content

Commit

Permalink
wallet-ext: sign message (MystenLabs#8918)
Browse files Browse the repository at this point in the history
## Description 

* wallet standard interface signMessage function
* reuse existing transaction requests for sign message requests
* started transitioning from transaction requests to approval requests



https://user-images.githubusercontent.com/10210143/223243789-d92965a1-3c1a-4499-a587-b22dff6a8080.mov



https://user-images.githubusercontent.com/10210143/223244077-38c17009-c2bd-4a9f-8142-1ab4e118de7e.mov


Button text updated Sign

<img width="366" alt="Screenshot 2023-03-07 at 22 13 29"
src="https://user-images.githubusercontent.com/10210143/223566219-74d4463f-4ced-426e-8ac3-d2847f9233ed.png">


## Test Plan 

manual, use example app in wallet-adapters to sign a test message

closes APPS-510
  • Loading branch information
pchrysochoidis authored Mar 7, 2023
1 parent f4e8edb commit 6e81387
Show file tree
Hide file tree
Showing 27 changed files with 577 additions and 346 deletions.
171 changes: 107 additions & 64 deletions apps/wallet/src/background/Transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,28 @@ import {
type SignedTransaction,
type SuiTransactionResponse,
} from '@mysten/sui.js';
import { type SuiSignMessageOutput } from '@mysten/wallet-standard';
import { filter, lastValueFrom, map, race, Subject, take } from 'rxjs';
import { v4 as uuidV4 } from 'uuid';
import Browser from 'webextension-polyfill';

import { Window } from './Window';
import {
type TransactionDataType,
type ApprovalRequest,
} from '_payloads/transactions/ApprovalRequest';
import { type SignMessageRequest } from '_payloads/transactions/SignMessage';

import type {
SuiSignTransactionSerialized,
TransactionDataType,
} from '_messages/payloads/transactions/ExecuteTransactionRequest';
import type { TransactionRequest } from '_payloads/transactions';
import type { SuiSignTransactionSerialized } from '_payloads/transactions/ExecuteTransactionRequest';
import type { TransactionRequestResponse } from '_payloads/transactions/ui/TransactionRequestResponse';
import type { ContentScriptConnection } from '_src/background/connections/ContentScriptConnection';

const TX_STORE_KEY = 'transactions';

function openTxWindow(txRequestID: string) {
function openTxWindow(requestID: string) {
return new Window(
Browser.runtime.getURL('ui.html') +
`#/dapp/tx-approval/${encodeURIComponent(txRequestID)}`
`#/dapp/approve/${encodeURIComponent(requestID)}`
);
}

Expand All @@ -43,64 +45,62 @@ class Transactions {
},
connection: ContentScriptConnection
): Promise<SuiTransactionResponse | SignedTransaction> {
const txRequest = this.createTransactionRequest(
tx ?? {
type: 'v2',
justSign: true,
data: sign.transaction,
account: sign.account,
},
const { txResultError, txResult, txSigned } =
await this.requestApproval(
tx ?? {
type: 'v2',
justSign: true,
data: sign.transaction,
account: sign.account,
},
connection.origin,
connection.originFavIcon
);
if (txResultError) {
throw new Error(
`Transaction failed with the following error. ${txResultError}`
);
}
if (sign && !txSigned) {
throw new Error('Transaction signature is empty');
}
if (tx) {
if (!txResult || !('transaction' in txResult)) {
throw new Error(`Transaction result is empty`);
}
return txResult;
}
return txSigned!;
}

public async signMessage(
{
accountAddress,
message,
}: Required<Pick<SignMessageRequest, 'args'>>['args'],
connection: ContentScriptConnection
): Promise<SuiSignMessageOutput> {
const { txResult, txResultError } = await this.requestApproval(
{ type: 'sign-message', accountAddress, message },
connection.origin,
connection.originFavIcon
);
await this.storeTransactionRequest(txRequest);
const popUp = openTxWindow(txRequest.id);
const popUpClose = (await popUp.show()).pipe(
take(1),
map<number, false>(() => false)
);
const txResponseMessage = this._txResponseMessages.pipe(
filter((msg) => msg.txID === txRequest.id),
take(1)
);
return lastValueFrom(
race(popUpClose, txResponseMessage).pipe(
take(1),
map(async (response) => {
if (response) {
const { approved, txResult, txSigned, tsResultError } =
response;
if (approved) {
txRequest.approved = approved;
txRequest.txResult = txResult;
txRequest.txResultError = tsResultError;
txRequest.txSigned = txSigned;
await this.storeTransactionRequest(txRequest);
if (tsResultError) {
throw new Error(
`Transaction failed with the following error. ${tsResultError}`
);
}
if (sign && !txSigned) {
throw new Error(
'Transaction signature is empty'
);
}
if (tx && !txResult) {
throw new Error(`Transaction result is empty`);
}
return tx ? txResult! : txSigned!;
}
}
await this.removeTransactionRequest(txRequest.id);
throw new Error('Transaction rejected from user');
})
)
);
if (txResultError) {
throw new Error(
`Signing message failed with the following error ${txResultError}`
);
}
if (!txResult) {
throw new Error('Sign message result is empty');
}
if (!('messageBytes' in txResult)) {
throw new Error('Sign message error, unknown result');
}
return txResult;
}

public async getTransactionRequests(): Promise<
Record<string, TransactionRequest>
Record<string, ApprovalRequest>
> {
return (await Browser.storage.local.get({ [TX_STORE_KEY]: {} }))[
TX_STORE_KEY
Expand All @@ -109,7 +109,7 @@ class Transactions {

public async getTransactionRequest(
txRequestID: string
): Promise<TransactionRequest | null> {
): Promise<ApprovalRequest | null> {
return (await this.getTransactionRequests())[txRequestID] || null;
}

Expand All @@ -118,10 +118,10 @@ class Transactions {
}

private createTransactionRequest(
tx: TransactionDataType,
tx: ApprovalRequest['tx'],
origin: string,
originFavIcon?: string
): TransactionRequest {
): ApprovalRequest {
return {
id: uuidV4(),
approved: null,
Expand All @@ -133,12 +133,12 @@ class Transactions {
}

private async saveTransactionRequests(
txRequests: Record<string, TransactionRequest>
txRequests: Record<string, ApprovalRequest>
) {
await Browser.storage.local.set({ [TX_STORE_KEY]: txRequests });
}

private async storeTransactionRequest(txRequest: TransactionRequest) {
private async storeTransactionRequest(txRequest: ApprovalRequest) {
const txs = await this.getTransactionRequests();
txs[txRequest.id] = txRequest;
await this.saveTransactionRequests(txs);
Expand All @@ -149,6 +149,49 @@ class Transactions {
delete txs[txID];
await this.saveTransactionRequests(txs);
}

private async requestApproval(
request: ApprovalRequest['tx'],
origin: string,
favIcon?: string
) {
const txRequest = this.createTransactionRequest(
request,
origin,
favIcon
);
await this.storeTransactionRequest(txRequest);
const popUp = openTxWindow(txRequest.id);
const popUpClose = (await popUp.show()).pipe(
take(1),
map<number, false>(() => false)
);
const txResponseMessage = this._txResponseMessages.pipe(
filter((msg) => msg.txID === txRequest.id),
take(1)
);
return lastValueFrom(
race(popUpClose, txResponseMessage).pipe(
take(1),
map(async (response) => {
if (response) {
const { approved, txResult, txSigned, txResultError } =
response;
if (approved) {
txRequest.approved = approved;
txRequest.txResult = txResult;
txRequest.txResultError = txResultError;
txRequest.txSigned = txSigned;
await this.storeTransactionRequest(txRequest);
return txRequest;
}
}
await this.removeTransactionRequest(txRequest.id);
throw new Error('Rejected from user');
})
)
);
}
}

export default new Transactions();
Loading

0 comments on commit 6e81387

Please sign in to comment.