Skip to content

Commit

Permalink
minor changes
Browse files Browse the repository at this point in the history
  • Loading branch information
quetool committed Jan 7, 2025
1 parent 70267d4 commit 4cd7588
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,42 @@ import 'package:pinenacl/x25519.dart';
import 'package:ed25519_edwards/ed25519_edwards.dart' as ed25519;

class PhantomHelper {
/// (required): A url used to fetch app metadata (i.e. title, icon) using the same properties found in Displaying Your App. URL-encoded.
/// (required): A url used to fetch app metadata (i.e. title, icon)
/// We do this provinding name, description, url and icons in PairingMetadata
/// Used in /connect URI to show the user this metadata in the approval modal
late final String _appUrl;

/// (required): The URI where Phantom should redirect the user upon connection. Please review Specifying Redirects for more details. URL-encoded.
/// (required): The URI Phantom Wallet calls when redirecting back after a request
/// We do this with the redirect object of PairingMetadata
/// Used in every URI
late final String _redirectLink;

/// (optional): The network that should be used for subsequent interactions. Can be either: mainnet-beta, testnet, or devnet. Defaults to mainnet-beta.
/// (optional): The network that should be used
/// Can be either: mainnet-beta, testnet, or devnet. Defaults to mainnet-beta
/// Used in /connect URI
late final String _cluster;

/// Private/Public keypair for encryption and decryption of data response
/// Private/Public keypair for encryption and decryption of phantom responses
/// `_privateKey` remains on dapp side and should be securely stored
late PrivateKey _privateKey;
String get selfPublicKey => solana.base58.encode(

/// `publicKey` is encoded in base58 and is sent in every URI/request
String get publicKey => solana.base58.encode(
_privateKey.publicKey.asTypedList,
);

/// [walletPublicKey] once session is established with Phantom Wallet (i.e. user has approved the connection) we get user's Publickey (the Solana address)
late final String walletPublicKey;
/// [solanaAddress] once session is established with Phantom Wallet we get user's wallet Publickey (the Solana address)
String solanaAddress = '';

/// When a user connects to Phantom Wallet for the first time, Phantom will return a session param that represents the user's connection.
/// When a user connects to Phantom Wallet for the first time, Phantom will return a session token param that represents the user's connection.
/// Will have to be securely stored
/// Is then used in the payload of every request/URI
String? _sessionToken;

/// [_sharedSecret] is used to encrypt and decrypt the session token and other data.
/// used to encrypt and decrypt the payload from and to Phantom Wallet
Box? _sharedSecret;

/// We need to provide [appUrl] and [redirectLink] as parameters to create new [PhantomHelper] object. [cluster] is optional
/// Initialization of `PhantomHelper` instance
PhantomHelper({
required String appUrl,
required String redirectLink,
Expand All @@ -48,7 +59,7 @@ class PhantomHelper {
host: 'phantom.app',
path: '/ul/v1/connect',
queryParameters: {
'dapp_encryption_public_key': selfPublicKey,
'dapp_encryption_public_key': publicKey,
'cluster': _cluster,
'app_url': _appUrl,
'redirect_link': '$_redirectLink?phantomRequest=connect',
Expand All @@ -58,14 +69,14 @@ class PhantomHelper {
/// Generate an URL to disconnect from Phantom Wallet and destroy the session.
Uri buildDisconnectUri() {
final payLoad = {'session': _sessionToken};
final encryptedPayload = _encryptPayload(payLoad);
final encryptedPayload = encryptPayload(payLoad);

final launchUri = Uri(
scheme: 'https',
host: 'phantom.app',
path: '/ul/v1/disconnect',
queryParameters: {
'dapp_encryption_public_key': selfPublicKey,
'dapp_encryption_public_key': publicKey,
'nonce': solana.base58.encode(encryptedPayload['nonce']),
'redirect_link': '$_redirectLink?phantomRequest=disconnect',
'payload': solana.base58.encode(encryptedPayload['encryptedPayload']),
Expand All @@ -77,7 +88,7 @@ class PhantomHelper {
return launchUri;
}

/// Generate an URL with given [transaction] to signAndSend transaction with Phantom Wallet.
/// Generate an URL with given [transaction] to signAndSend it with Phantom Wallet.
Uri buildSignAndSendTransactionUri({required String transaction}) {
final payload = {
'session': _sessionToken,
Expand All @@ -87,22 +98,22 @@ class PhantomHelper {
),
),
};
final encryptedPayload = _encryptPayload(payload);
final encryptedPayload = encryptPayload(payload);

return Uri(
scheme: 'https',
host: 'phantom.app',
path: '/ul/v1/signAndSendTransaction',
queryParameters: {
'dapp_encryption_public_key': selfPublicKey,
'dapp_encryption_public_key': publicKey,
'nonce': solana.base58.encode(encryptedPayload['nonce']),
'redirect_link': '$_redirectLink?phantomRequest=signAndSendTransaction',
'payload': solana.base58.encode(encryptedPayload['encryptedPayload'])
},
);
}

/// Generate an URL with given [transaction] to sign transaction with Phantom Wallet.
/// Generate an URL with given [transaction] to sign it with Phantom Wallet.
Uri buildSignTransactionUri({required String transaction}) {
final payload = {
'transaction': solana.base58.encode(
Expand All @@ -112,22 +123,22 @@ class PhantomHelper {
),
'session': _sessionToken,
};
final encryptedPayload = _encryptPayload(payload);
final encryptedPayload = encryptPayload(payload);

return Uri(
scheme: 'https',
host: 'phantom.app',
path: '/ul/v1/signTransaction',
queryParameters: {
'dapp_encryption_public_key': selfPublicKey,
'dapp_encryption_public_key': publicKey,
'nonce': solana.base58.encode(encryptedPayload['nonce']),
'redirect_link': '$_redirectLink?phantomRequest=signTransaction',
'payload': solana.base58.encode(encryptedPayload['encryptedPayload'])
},
);
}

/// Generate an URL with given [transactions] to sign all transaction with Phantom Wallet.
/// Generate an URL with given [transactions] to sign all with Phantom Wallet.
Uri buildUriSignAllTransactions({required List<String> transactions}) {
final payload = {
'transactions': transactions
Expand All @@ -139,109 +150,101 @@ class PhantomHelper {
.toList(),
'session': _sessionToken,
};
final encryptedPayload = _encryptPayload(payload);
final encryptedPayload = encryptPayload(payload);

return Uri(
scheme: 'https',
host: 'phantom.app',
path: '/ul/v1/signAllTransactions',
queryParameters: {
'dapp_encryption_public_key': selfPublicKey,
'dapp_encryption_public_key': publicKey,
'nonce': solana.base58.encode(encryptedPayload['nonce']),
'redirect_link': '$_redirectLink?phantomRequest=signAllTransactions',
'payload': solana.base58.encode(encryptedPayload['encryptedPayload'])
},
);
}

/// Generates an URL with given [nonce] to be signed by Phantom Wallet to verify the ownership of the wallet.
/// Generates an URL with given [nonce] to be signed by Phantom Wallet
/// If `nonce` is not passed it will generate one
Uri buildSignMessageUri({required String message, Uint8List? nonce}) {
/// Hash the nonce so that it is not exposed to the user
final hashedNonce = Hash.sha256(nonce ?? PineNaClUtils.randombytes(24));

final m = '$message Nonce: ${solana.base58.encode(hashedNonce)}';
final msg = '$message Nonce: ${solana.base58.encode(hashedNonce)}';
final payload = {
'session': _sessionToken,
'message': solana.base58.encode(m.codeUnits.toUint8List()),
'message': solana.base58.encode(
msg.codeUnits.toUint8List(),
),
};

final encryptedPayload = _encryptPayload(payload);
final encryptedPayload = encryptPayload(payload);

return Uri(
scheme: 'https',
host: 'phantom.app',
path: 'ul/v1/signMessage',
queryParameters: {
'dapp_encryption_public_key': selfPublicKey,
'dapp_encryption_public_key': publicKey,
'nonce': solana.base58.encode(encryptedPayload['nonce']),
'redirect_link': '$_redirectLink?phantomRequest=signMessage',
'payload': solana.base58.encode(encryptedPayload['encryptedPayload']),
},
);
}

/// Creates [_sharedSecret] using [_privateKey] and [phantom_encryption_public_key].
bool createSession(Map<String, String> params) {
try {
// `phantom_encryption_public_key` is the public key of Phantom Wallet.
final phantomPublicKey = solana.base58.decode(
params['phantom_encryption_public_key']!,
);

// Created a shared secret between Phantom Wallet and our DApp using our [_privateKey] and [phantom_encryption_public_key].
_sharedSecret = Box(
myPrivateKey: _privateKey,
theirPublicKey: PublicKey(phantomPublicKey),
);
final decryptedData = decryptPayload(
data: params['data']!,
nonce: params['nonce']!,
);
_sessionToken = decryptedData['session'];
walletPublicKey = decryptedData['public_key'];
} catch (e) {
return false;
}
return true;
}

/// Verifies the [signature] returned by Phantom Wallet.
// TODO this can be achieve with core package probably
Future<bool> validateSignature(String signature, Uint8List nonce) async {
Uint8List hashedNonce = Hash.sha256(nonce);
final message =
'Sign this message for authenticating with your wallet. Nonce: ${solana.base58.encode(hashedNonce)}';
Future<bool> validateSignature(String message, String signature) async {
final messageBytes = message.codeUnits.toUint8List();
final signatureBytes = solana.base58.decode(signature);

final verify = ed25519.verify(
ed25519.PublicKey(solana.base58.decode(walletPublicKey)),
return ed25519.verify(
ed25519.PublicKey(solana.base58.decode(solanaAddress)),
messageBytes,
signatureBytes,
);
nonce = Uint8List(0);
return verify;
}

void _createSharedSecret({required String phantomPublicKey}) {
final phPublicKey = solana.base58.decode(phantomPublicKey);
// Create a shared secret between Phantom Wallet and our DApp using our [_privateKey] and [phantom_encryption_public_key].
_sharedSecret = Box(
myPrivateKey: _privateKey,
theirPublicKey: PublicKey(phPublicKey),
);
}

/// Decrypts the [data] payload returned by Phantom Wallet
Map<dynamic, dynamic> decryptPayload({
required String data,
required String nonce,
}) {
if (_sharedSecret == null) {
return <String, String>{};
Map<dynamic, dynamic> decryptPayload({required Map<String, String> params}) {
if (params.containsKey('phantom_encryption_public_key')) {
// meaning is /connect request
// phantom_encryption_public_key is Phantom publicKey (not solana Address)
final phantomPublicKey = params['phantom_encryption_public_key']!;
// executed only once after successful /connect
_createSharedSecret(phantomPublicKey: phantomPublicKey);
}

// data and nonce are always present
final data = params['data']!;
final nonce = params['nonce']!;
final decryptedData = _sharedSecret?.decrypt(
ByteList(solana.base58.decode(data)),
nonce: Uint8List.fromList(solana.base58.decode(nonce)),
);

return JsonDecoder().convert(String.fromCharCodes(decryptedData!));
final payload = JsonDecoder().convert(String.fromCharCodes(
decryptedData!,
));

_sessionToken = payload['session'] ?? _sessionToken;
solanaAddress = payload['public_key'] ?? solanaAddress;

return payload;
}

/// Encrypts the data payload to be sent to Phantom Wallet.
Map<String, dynamic> _encryptPayload(Map<String, dynamic> data) {
Map<String, dynamic> encryptPayload(Map<String, dynamic> data) {
if (_sharedSecret == null) {
return <String, String>{};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:get_it/get_it.dart';

Expand Down Expand Up @@ -50,16 +47,8 @@ class PhantomService implements IPhantomService {
);

@override
List<String> get supportedMethods => [
...MethodsConstants.requiredMethods,
'eth_requestAccounts',
'eth_signTypedData_v3',
'eth_signTypedData_v4',
'eth_signTransaction',
MethodsConstants.walletSwitchEthChain,
MethodsConstants.walletAddEthChain,
'wallet_watchAsset',
];
List<String> get supportedMethods =>
NetworkUtils.defaultNetworkMethods['solana']!.toList();

@override
Event<PhantomConnectEvent> onPhantomConnect = Event<PhantomConnectEvent>();
Expand Down Expand Up @@ -116,20 +105,17 @@ class PhantomService implements IPhantomService {
}

@override
Future<String> get ownPublicKey async {
return _phantomHelper.selfPublicKey;
}
Future<String> get ownPublicKey async => _phantomHelper.publicKey;

@override
Future<String> get peerPublicKey async {
return _phantomHelper.walletPublicKey;
}
Future<String> get peerPublicKey async => _phantomHelper.solanaAddress;

@override
Future<void> getAccount() async {
await _checkInstalled();
try {
final phantomUri = _phantomHelper.buildConnectionUri();
print('[$runtimeType] $phantomUri');
await ReownCoreUtils.openURL(phantomUri.toString());
} on PlatformException catch (e, s) {
_core.logger.e('[$runtimeType] getAccount PlatformException $e');
Expand All @@ -145,31 +131,23 @@ class PhantomService implements IPhantomService {
}

@override
void completePhantomRequest({required String url}) {
void completePhantomRequest({required String url}) async {
final params = Uri.parse(url).queryParameters;
final phantomRequest = ReownCoreUtils.getSearchParamFromURL(
final request = ReownCoreUtils.getSearchParamFromURL(
url,
'phantomRequest',
);
final phantomNonce = ReownCoreUtils.getSearchParamFromURL(url, 'nonce');
final phantomData = ReownCoreUtils.getSearchParamFromURL(url, 'data');

if (phantomRequest == 'connect') {
if (_phantomHelper.createSession(params)) {
_core.logger.i('[$runtimeType] Phantom Wallet connected $params');
// onPhantomConnect.broadcast(PhantomConnectEvent(...));
}
// TODO for testing purpose
final signUri = _phantomHelper.buildSignMessageUri(
message: 'AppKit flutter message from Solana.',
);
ReownCoreUtils.openURL(signUri.toString());
} else {
final payload = _phantomHelper.decryptPayload(
data: phantomData,
nonce: phantomNonce,

final payload = _phantomHelper.decryptPayload(params: params);
print('[$runtimeType] payload $payload');

if (request == 'connect') {
// TODO create session
final signMessageUri = _phantomHelper.buildSignMessageUri(
message: 'Sign this message',
);
debugPrint(jsonEncode(payload));
print('[$runtimeType] $signMessageUri');
await ReownCoreUtils.openURL(signMessageUri.toString());
}
}

Expand Down

0 comments on commit 4cd7588

Please sign in to comment.