Skip to content

Commit

Permalink
feat: teach mobile-wallet-adapter to handle versioned transactions (#254
Browse files Browse the repository at this point in the history
)

* chore: upgrade to version of `@solana/web3.js` that supports versioned transactions

* chore: advance version of `@solana/wallet-adapter-base` to one that supports versioned transactions

* fix: accept versioned transactions
  • Loading branch information
steveluscher authored Sep 26, 2022
1 parent bed242f commit 4d12a3c
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 370 deletions.
4 changes: 2 additions & 2 deletions examples/example-react-native-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"@react-native-async-storage/async-storage": "^1.17.7",
"@solana-mobile/mobile-wallet-adapter-protocol": "file:../../js/packages/mobile-wallet-adapter-protocol",
"@solana-mobile/mobile-wallet-adapter-protocol-web3js": "file:../../js/packages/mobile-wallet-adapter-protocol-web3js",
"@solana/wallet-adapter-base": "^0.9.8",
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.7",
"@solana/web3.js": "^1.54.1",
"@solana/web3.js": "^1.58.0",
"js-base64": "^3.7.2",
"localstorage-polyfill": "^1.0.1",
"react": "^18.1.0",
Expand Down
16 changes: 8 additions & 8 deletions examples/example-react-native-app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1358,10 +1358,10 @@
dependencies:
buffer "~6.0.3"

"@solana/wallet-adapter-base@^0.9.16", "@solana/wallet-adapter-base@^0.9.8":
version "0.9.16"
resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.16.tgz#7b67e145f47a9cdc8e702eacc02fa867084a9bf4"
integrity sha512-nwzpzo3SjjsCkgN5ROQqoL7Y90j4QUlqxL17sg/qMcoYIBVfalyu87IZAfL5B06eSQ8jmtgnuQJW+91WcLo1ag==
"@solana/wallet-adapter-base@^0.9.16", "@solana/wallet-adapter-base@^0.9.17":
version "0.9.17"
resolved "https://registry.yarnpkg.com/@solana/wallet-adapter-base/-/wallet-adapter-base-0.9.17.tgz#b388fea0ec6da40e23342068a4cfa9be65dc8f63"
integrity sha512-YEkO04QndfRXb6psznMuRsw2YBHqVGxmuJgQskCHp2DAkHWPDNbKlv+Q4mOD2gfkUNHUMP8sTnwORhsIR3fQjQ==
dependencies:
eventemitter3 "^4.0.0"

Expand All @@ -1372,10 +1372,10 @@
dependencies:
"@solana/wallet-adapter-base" "^0.9.16"

"@solana/web3.js@^1.54.1":
version "1.60.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.60.0.tgz#279dd95ab60d24c32dec5337b37db0d82e854bdd"
integrity sha512-gXwUPOruR786Mbce4n5cM2JA00UvRLuoUAQ5Me/XvY49Tqb8u4umifPY/rzWigJxs3XDCN2i2OT1avYjoePLMw==
"@solana/web3.js@^1.58.0":
version "1.63.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.63.0.tgz#2e247d89785bd79c994bbc79040f0d146ee117a5"
integrity sha512-qmDmfLPDjzhTAfN3jbtVI8YaIIIDN9q4p6gCiIhWwzKs8299rq8B+tkmCgiU6tAtIm3uqHl3wduhJMKmqdToBA==
dependencies:
"@babel/runtime" "^7.12.5"
"@noble/ed25519" "^1.7.0"
Expand Down
5 changes: 2 additions & 3 deletions examples/example-web-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@
"@solana-mobile/mobile-wallet-adapter-protocol": "link:../../js/packages/mobile-wallet-adapter-protocol",
"@solana-mobile/mobile-wallet-adapter-protocol-web3js": "link:../../js/packages/mobile-wallet-adapter-protocol-web3js",
"@solana-mobile/wallet-adapter-mobile": "link:../../js/packages/wallet-adapter-mobile",
"@solana/wallet-adapter-base": "^0.9.8",
"@solana/wallet-adapter-base": "^0.9.17",
"@solana/wallet-adapter-react": "^0.15.7",
"@solana/wallet-adapter-react-ui": "^0.9.7",
"@solana/web3.js": "^1.54.1",
"@solana/web3.js": "^1.58.0",
"next": "^12.1.6",
"notistack": "^2.0.5",
"react": "^18.0.0",
Expand Down
190 changes: 18 additions & 172 deletions examples/example-web-app/yarn.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"prepublishOnly": "agadoo"
},
"peerDependencies": {
"@solana/web3.js": "^1.48.0"
"@solana/web3.js": "^1.58.0"
},
"dependencies": {
"@solana-mobile/mobile-wallet-adapter-protocol": "^0.9.1",
Expand Down
58 changes: 35 additions & 23 deletions js/packages/mobile-wallet-adapter-protocol-web3js/src/transact.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Transaction, TransactionSignature } from '@solana/web3.js';
import {
Transaction as LegacyTransaction,
Transaction,
TransactionSignature,
VersionedMessage,
VersionedTransaction,
} from '@solana/web3.js';
import {
AuthorizeAPI,
Base64EncodedAddress,
Base64EncodedTransaction,
CloneAuthorizationAPI,
DeauthorizeAPI,
MobileWallet,
Expand All @@ -14,14 +21,14 @@ import bs58 from 'bs58';
import { fromUint8Array, toUint8Array } from './base64Utils';

interface Web3SignAndSendTransactionsAPI {
signAndSendTransactions(params: {
signAndSendTransactions<T extends LegacyTransaction | VersionedTransaction>(params: {
minContextSlot?: number;
transactions: Transaction[];
transactions: T[];
}): Promise<TransactionSignature[]>;
}

interface Web3SignTransactionsAPI {
signTransactions(params: { transactions: Transaction[] }): Promise<Transaction[]>;
signTransactions<T extends LegacyTransaction | VersionedTransaction>(params: { transactions: T[] }): Promise<T[]>;
}

interface Web3SignMessagesAPI {
Expand All @@ -37,6 +44,27 @@ export interface Web3MobileWallet
Web3SignTransactionsAPI,
Web3SignMessagesAPI {}

function getPayloadFromTransaction(transaction: LegacyTransaction | VersionedTransaction): Base64EncodedTransaction {
const serializedTransaction =
'version' in transaction
? transaction.serialize()
: transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
const payload = fromUint8Array(serializedTransaction);
return payload;
}

function getTransactionFromWireMessage(byteArray: Uint8Array): Transaction | VersionedTransaction {
const version = VersionedMessage.deserializeMessageVersion(byteArray);
if (version === 'legacy') {
return Transaction.from(byteArray);
} else {
return VersionedTransaction.deserialize(byteArray);
}
}

export async function transact<TReturn>(
callback: (wallet: Web3MobileWallet) => TReturn,
config?: WalletAssociationConfig,
Expand All @@ -52,15 +80,7 @@ export async function transact<TReturn>(
transactions,
...rest
}: Parameters<Web3MobileWallet['signAndSendTransactions']>[0]) {
const payloads = await Promise.all(
transactions.map(async (transaction) => {
const serializedTransaction = await transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
return serializedTransaction.toString('base64');
}),
);
const payloads = transactions.map(getPayloadFromTransaction);
const { signatures: base64EncodedSignatures } = await wallet.signAndSendTransactions({
...rest,
...(minContextSlot != null
Expand Down Expand Up @@ -91,22 +111,14 @@ export async function transact<TReturn>(
transactions,
...rest
}: Parameters<Web3MobileWallet['signTransactions']>[0]) {
const serializedTransactions = transactions.map((transaction) =>
transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
}),
);
const payloads = serializedTransactions.map((serializedTransaction) =>
serializedTransaction.toString('base64'),
);
const payloads = transactions.map(getPayloadFromTransaction);
const { signed_payloads: base64EncodedCompiledTransactions } =
await wallet.signTransactions({
...rest,
payloads,
});
const compiledTransactions = base64EncodedCompiledTransactions.map(toUint8Array);
const signedTransactions = compiledTransactions.map(Transaction.from);
const signedTransactions = compiledTransactions.map(getTransactionFromWireMessage);
return signedTransactions;
} as Web3MobileWallet[TMethodName];
break;
Expand Down
2 changes: 1 addition & 1 deletion js/packages/mobile-wallet-adapter-protocol/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type Base64EncodedSignedMessage = string;

type Base64EncodedSignedTransaction = string;

type Base64EncodedTransaction = string;
export type Base64EncodedTransaction = string;

export type Cluster = 'devnet' | 'testnet' | 'mainnet-beta';

Expand Down
6 changes: 3 additions & 3 deletions js/packages/wallet-adapter-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@
"prepublishOnly": "agadoo"
},
"peerDependencies": {
"@solana/web3.js": "^1.36.0"
"@solana/web3.js": "^1.58.0"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.17.7",
"@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^0.9.1",
"@solana/wallet-adapter-base": "^0.9.8",
"@solana/web3.js": "^1.20.0",
"@solana/wallet-adapter-base": "^0.9.17",
"js-base64": "^3.7.2"
},
"devDependencies": {
"@solana/web3.js": "^1.58.0",
"agadoo": "^2.0.0",
"cross-env": "^7.0.3",
"shx": "^0.3.4"
Expand Down
121 changes: 73 additions & 48 deletions js/packages/wallet-adapter-mobile/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ import {
WalletSignMessageError,
WalletSignTransactionError,
} from '@solana/wallet-adapter-base';
import { Connection, PublicKey, SendOptions, Transaction, TransactionSignature } from '@solana/web3.js';
import {
Connection,
PublicKey,
SendOptions,
Transaction as LegacyTransaction,
TransactionSignature,
TransactionVersion,
VersionedTransaction,
} from '@solana/web3.js';
import { toUint8Array } from './base64Utils';
import getIsSupported from './getIsSupported';
import { Cluster } from '@solana-mobile/mobile-wallet-adapter-protocol';
Expand All @@ -44,6 +52,10 @@ function getPublicKeyFromAddress(address: Base64EncodedAddress): PublicKey {
}

export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
readonly supportedTransactionVersions: Set<TransactionVersion> = new Set(
// FIXME(#244): We can't actually know what versions are supported until we know which wallet we're talking to.
['legacy', 0],
);
name = SolanaMobileWalletAdapterWalletName;
url = 'https://solanamobile.com';
icon =
Expand Down Expand Up @@ -217,7 +229,9 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
};
}

private async performSignTransactions(transactions: Transaction[]): Promise<Transaction[]> {
private async performSignTransactions<T extends LegacyTransaction | VersionedTransaction>(
transactions: T[],
): Promise<T[]> {
const { authToken } = this.assertIsAuthorized();
try {
return await this.transact(async (wallet) => {
Expand All @@ -232,8 +246,8 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
}
}

async sendTransaction(
transaction: Transaction,
async sendTransaction<T extends LegacyTransaction | VersionedTransaction>(
transaction: T,
connection: Connection,
options?: SendOptions,
): Promise<TransactionSignature> {
Expand All @@ -242,51 +256,62 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
const minContextSlot = options?.minContextSlot;
try {
return await this.transact(async (wallet) => {
let targetCommitment: Finality;
switch (connection.commitment) {
case 'confirmed':
case 'finalized':
case 'processed':
targetCommitment = connection.commitment;
break;
default:
targetCommitment = 'finalized';
}
let targetPreflightCommitment: Finality;
switch (options?.preflightCommitment) {
case 'confirmed':
case 'finalized':
case 'processed':
targetPreflightCommitment = options.preflightCommitment;
break;
case undefined:
targetPreflightCommitment = targetCommitment;
default:
targetPreflightCommitment = 'finalized';
}
await Promise.all([
this.performReauthorization(wallet, authToken),
(async () => {
if (transaction.recentBlockhash == null) {
const preflightCommitmentScore =
targetPreflightCommitment === 'finalized'
? 2
: targetPreflightCommitment === 'confirmed'
? 1
: 0;
const targetCommitmentScore =
targetCommitment === 'finalized' ? 2 : targetCommitment === 'confirmed' ? 1 : 0;
const { blockhash } = await connection.getLatestBlockhash({
commitment:
preflightCommitmentScore < targetCommitmentScore
? targetPreflightCommitment
: targetCommitment,
});
transaction.recentBlockhash = blockhash;
}
})(),
'version' in transaction
? null
: /**
* Unlike versioned transactions, legacy `Transaction` objects
* may not have an associated `feePayer` or `recentBlockhash`.
* This code exists to patch them up in case they are missing.
*/
(async () => {
transaction.feePayer ||= this.publicKey ?? undefined;
if (transaction.recentBlockhash == null) {
let targetCommitment: Finality;
switch (connection.commitment) {
case 'confirmed':
case 'finalized':
case 'processed':
targetCommitment = connection.commitment;
break;
default:
targetCommitment = 'finalized';
}
let targetPreflightCommitment: Finality;
switch (options?.preflightCommitment) {
case 'confirmed':
case 'finalized':
case 'processed':
targetPreflightCommitment = options.preflightCommitment;
break;
case undefined:
targetPreflightCommitment = targetCommitment;
default:
targetPreflightCommitment = 'finalized';
}
const preflightCommitmentScore =
targetPreflightCommitment === 'finalized'
? 2
: targetPreflightCommitment === 'confirmed'
? 1
: 0;
const targetCommitmentScore =
targetCommitment === 'finalized'
? 2
: targetCommitment === 'confirmed'
? 1
: 0;
const { blockhash } = await connection.getLatestBlockhash({
commitment:
preflightCommitmentScore < targetCommitmentScore
? targetPreflightCommitment
: targetCommitment,
});
transaction.recentBlockhash = blockhash;
}
})(),
]);
transaction.feePayer ||= this.publicKey ?? undefined;
const signatures = await wallet.signAndSendTransactions({
minContextSlot,
transactions: [transaction],
Expand All @@ -299,14 +324,14 @@ export class SolanaMobileWalletAdapter extends BaseMessageSignerWalletAdapter {
});
}

async signTransaction(transaction: Transaction): Promise<Transaction> {
async signTransaction<T extends LegacyTransaction | VersionedTransaction>(transaction: T): Promise<T> {
return await this.runWithGuard(async () => {
const [signedTransaction] = await this.performSignTransactions([transaction]);
return signedTransaction;
});
}

async signAllTransactions(transactions: Transaction[]): Promise<Transaction[]> {
async signAllTransactions<T extends LegacyTransaction | VersionedTransaction>(transactions: T[]): Promise<T[]> {
return await this.runWithGuard(async () => {
const signedTransactions = await this.performSignTransactions(transactions);
return signedTransactions;
Expand Down
Loading

0 comments on commit 4d12a3c

Please sign in to comment.