Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New wallet adapter interface #473

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ coverage
.next/
out/
build
dist/

# misc
.DS_Store
Expand Down
1 change: 1 addition & 0 deletions packages/wallet-adapter-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { WALLET_ADAPTER_CORE_VERSION } from "./version";

export { type AnyAptosWallet, type DappConfig, WalletCore } from "./WalletCore";
export * from "./LegacyWalletPlugins";
export * from "./new";
export * from "./constants";
export * from "./utils";
export * from "./AIP62StandardWallets";
Expand Down
228 changes: 228 additions & 0 deletions packages/wallet-adapter-core/src/new/AdaptedWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { Aptos, AptosConfig } from '@aptos-labs/ts-sdk';
import {
AccountInfo,
AptosFeatures,
AptosSignAndSubmitTransactionInput,
AptosSignAndSubmitTransactionOutput,
AptosSignMessageInput,
AptosSignTransactionInputV1_1,
AptosSignTransactionOutput,
AptosSignTransactionOutputV1_1,
WalletIcon,
} from '@aptos-labs/wallet-standard';
import { GA4 } from '../ga';
import { WALLET_ADAPTER_CORE_VERSION } from '../version';
import { AptosStandardWallet } from './AptosStandardWallet';
import { Network } from './network';
import {
aptosChainIdentifierToNetworkMap,
buildTransaction,
chainIdToStandardNetwork,
isFeatureMinorVersion,
mapUserResponse,
networkInfoToNetwork,
} from './utils';

type EventHandlers = {
accountDisconnected: (account: AccountInfo) => void;
activeAccountChanged: (account?: AccountInfo) => void;
activeNetworkChanged: (network: Network) => void;
}

export interface AdaptedWalletConfig {
disableTelemetry?: boolean;
}

/**
* A wallet instance adapted from an Aptos standard wallet that supports
* all required features with minimum version.
*/
export class AdaptedWallet {
readonly name: string;
readonly url: string;
readonly icon: WalletIcon;
readonly features: AptosFeatures;

readonly availableNetworks: Network[];

// Google Analytics 4 module
private readonly ga4?: GA4;

constructor(wallet: AptosStandardWallet, options: AdaptedWalletConfig = {}) {
this.name = wallet.name;
this.url = wallet.url;
this.icon = wallet.icon;
this.features = wallet.features;

if (!options.disableTelemetry) {
this.ga4 = new GA4();
}

this.availableNetworks = [];
for (const chain of wallet.chains) {
const network = aptosChainIdentifierToNetworkMap[chain];
if (network) {
this.availableNetworks.push(network);
}
}
}

// TODO: revise event formats and names
private recordEvent(eventName: string, additionalInfo?: object) {
this.ga4?.gtag("event", `wallet_adapter_${eventName}`, {
wallet: this.name,
// network: this._network?.name,
// network_url: this._network?.url,
adapter_core_version: WALLET_ADAPTER_CORE_VERSION,
send_to: process.env.GAID,
...additionalInfo,
});
}

// region Connection

async connect() {
const feature = this.features['aptos:connect'];
return feature.connect();
}

async disconnect() {
// TODO: specify which account. defaults to active account
const feature = this.features['aptos:disconnect'];
await feature.disconnect();
}

// endregion

// region Accounts

async getConnectedAccounts(): Promise<AccountInfo[]> {
// TODO: add explicit `getConnectedAccounts` feature
const activeAccount = await this.getActiveAccount();
return activeAccount ? [activeAccount] : [];
}

// TODO
onAccountDisconnected(callback: (account: AccountInfo) => void) {
return () => {
};
}

async getActiveAccount(): Promise<AccountInfo | undefined> {
return this.features['aptos:account'].account()
.catch(() => undefined);
}

onActiveAccountChanged(callback: (account?: AccountInfo) => void) {
const feature = this.features['aptos:onAccountChange'];
void feature.onAccountChange((newAccount) => {
callback(newAccount);
});
return () => {
void feature.onAccountChange(() => {
});
}
}

// endregion

// region Networks

async getAvailableNetworks(): Promise<Network[]> {
// TODO: maybe add explicit `getAvailableNetworks` feature
return this.availableNetworks;
}

async getActiveNetwork(): Promise<Network> {
const feature = this.features['aptos:network'];
const networkInfo = await feature.network();
return networkInfoToNetwork(networkInfo);
}

onActiveNetworkChanged(callback: (network: Network) => void) {
const feature = this.features['aptos:onNetworkChange'];

void feature.onNetworkChange((networkInfo) => {
const network = networkInfoToNetwork(networkInfo);
callback(network);
});
return () => {
void feature.onNetworkChange(() => {
});
}
}

// endregion

// region Signature

// TODO: improve message signature standard
async signMessage(input: AptosSignMessageInput) {
const feature = this.features['aptos:signMessage'];
return feature.signMessage(input);
}

async signTransaction(input: AptosSignTransactionInputV1_1) {
const feature = this.features['aptos:signTransaction']

if (isFeatureMinorVersion(feature, "1.0")) {
const { signerAddress, feePayer } = input;
// This will throw an error if it requires an async call
const transaction = buildTransaction(input);
const asFeePayer = signerAddress?.toString() === feePayer?.address.toString();
const response = await feature.signTransaction(transaction, asFeePayer);

return mapUserResponse<AptosSignTransactionOutput, AptosSignTransactionOutputV1_1>(
response, (authenticator) => ({
authenticator,
rawTransaction: transaction,
}));
}

return feature.signTransaction(input);
}

async signAndSubmitTransaction(input: AptosSignAndSubmitTransactionInput) {
const feature = this.features['aptos:signAndSubmitTransaction']
if (feature) {
return feature.signAndSubmitTransaction(input);
}

const response = await this.signTransaction(input);
return mapUserResponse<AptosSignTransactionOutputV1_1, AptosSignAndSubmitTransactionOutput>(
response, async ({ rawTransaction: transaction, authenticator }) => {
const { chainId } = transaction.rawTransaction.chain_id;
const network = chainIdToStandardNetwork(chainId);
const aptosConfig = new AptosConfig({ network });
const aptosClient = new Aptos(aptosConfig);

const { hash } = await aptosClient.transaction.submit.simple({
transaction,
senderAuthenticator: authenticator,
});
return { hash };
});
}

// endregion

// region Event handling

on<EventName extends keyof EventHandlers>(eventName: EventName, callback: EventHandlers[EventName]) {
const handlers: {
[K in keyof EventHandlers]: (cb: EventHandlers[K]) => () => void;
} = {
accountDisconnected: (cb) => this.onAccountDisconnected(cb),
activeAccountChanged: (cb) => this.onActiveAccountChanged(cb),
activeNetworkChanged: (cb) => this.onActiveNetworkChanged(cb),
};

const handler = handlers[eventName];
if (!handler) {
throw new Error('Unsupported event name');
}
return handler(callback);
}

// endregion
}
52 changes: 52 additions & 0 deletions packages/wallet-adapter-core/src/new/AptosStandardWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
AptosFeatures,
AptosWallet as AptosStandardWallet,
MinimallyRequiredFeatures,
Wallet,
} from '@aptos-labs/wallet-standard';

type FeatureVersion = `${number}.${number}` | `${number}.${number}.${number}`;
type TargetVersion = `${number}.${number}`;

/**
* Required features with minimum versions.
* In the future, we might choose to slowly deprecate older versions to simplify the adapter's code.
*/
const requiredFeatures: [name: keyof MinimallyRequiredFeatures, version: TargetVersion][] = [
['aptos:account', '1.0'],
['aptos:connect', '1.0'],
['aptos:disconnect', '1.0'],
['aptos:network', '1.0'],
['aptos:onAccountChange', '1.0'],
['aptos:onNetworkChange', '1.0'],
['aptos:signMessage', '1.0'],
['aptos:signTransaction', '1.0'],
];

/**
* Check whether the specified version is compatible with a target version
*/
function isVersionCompatible(value: FeatureVersion, target: TargetVersion) {
const [major, minor] = value.split('.').map(Number);
const [tgtMajor, tgtMinor] = target.split('.').map(Number);
return major === tgtMajor && minor >= tgtMinor;
}

/**
* Check whether a generic wallet is an Aptos standard wallet.
*
* The wallet needs to implement all the required features with minimum version.
* @param wallet generic wallet to be considered compatible.
*/
export function isAptosStandardWallet(wallet: Wallet): wallet is AptosStandardWallet {
const features = wallet.features as Partial<AptosFeatures>;
for (const [name, targetVersion] of requiredFeatures) {
const feature = features[name];
if (!feature || !isVersionCompatible(feature.version, targetVersion)) {
return false;
}
}
return true;
}

export type { AptosStandardWallet };
Loading