From 6c985a081d84f260e916e2b05d8d023662b80eda Mon Sep 17 00:00:00 2001 From: navidhosseini Date: Sat, 26 Nov 2022 12:41:04 +0330 Subject: [PATCH] init --- .flutter-plugins | 6 + .flutter-plugins-dependencies | 1 + .gitignore | 30 ++++ .metadata | 10 ++ .vscode/settings.json | 5 + CHANGELOG.md | 3 + LICENSE | 1 + README.md | 39 +++++ analysis_options.yaml | 4 + lib/flutter_phantom.dart | 3 + lib/src/flutter_phantom.dart | 301 +++++++++++++++++++++++++++++++++ pubspec.yaml | 22 +++ test/flutter_phantom_test.dart | 1 + 13 files changed, 426 insertions(+) create mode 100644 .flutter-plugins create mode 100644 .flutter-plugins-dependencies create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 .vscode/settings.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 lib/flutter_phantom.dart create mode 100644 lib/src/flutter_phantom.dart create mode 100644 pubspec.yaml create mode 100644 test/flutter_phantom_test.dart diff --git a/.flutter-plugins b/.flutter-plugins new file mode 100644 index 0000000..f374d73 --- /dev/null +++ b/.flutter-plugins @@ -0,0 +1,6 @@ +# This is a generated file; do not edit or check into version control. +connectivity_plus=/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.9/ +connectivity_plus_linux=/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/ +connectivity_plus_macos=/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.6/ +connectivity_plus_web=/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.5/ +connectivity_plus_windows=/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/ diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies new file mode 100644 index 0000000..3da700d --- /dev/null +++ b/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.9/","native_build":true,"dependencies":[]}],"android":[{"name":"connectivity_plus","path":"/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus-2.3.9/","native_build":true,"dependencies":[]}],"macos":[{"name":"connectivity_plus_macos","path":"/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_macos-1.2.6/","native_build":true,"dependencies":[]}],"linux":[{"name":"connectivity_plus_linux","path":"/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_linux-1.3.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"connectivity_plus_windows","path":"/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_windows-1.2.2/","native_build":true,"dependencies":[]}],"web":[{"name":"connectivity_plus_web","path":"/home/arj-co-navid/.pub-cache/hosted/pub.dartlang.org/connectivity_plus_web-1.2.5/","dependencies":[]}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":["connectivity_plus_linux","connectivity_plus_macos","connectivity_plus_web","connectivity_plus_windows"]},{"name":"connectivity_plus_linux","dependencies":[]},{"name":"connectivity_plus_macos","dependencies":[]},{"name":"connectivity_plus_web","dependencies":[]},{"name":"connectivity_plus_windows","dependencies":[]}],"date_created":"2022-11-26 11:27:00.887983","version":"3.4.0-19.0.pre.86"} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96486fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..7e644a7 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: a04e6b7618700f7b1cadee23f82ef11e693ef44c + channel: master + +project_type: package diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c234477 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "randombytes" + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md new file mode 100644 index 0000000..02fe8ec --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/lib/flutter_phantom.dart b/lib/flutter_phantom.dart new file mode 100644 index 0000000..9aac477 --- /dev/null +++ b/lib/flutter_phantom.dart @@ -0,0 +1,3 @@ +library flutter_phantom; + +export 'src/flutter_phantom.dart'; diff --git a/lib/src/flutter_phantom.dart b/lib/src/flutter_phantom.dart new file mode 100644 index 0000000..caaf5cb --- /dev/null +++ b/lib/src/flutter_phantom.dart @@ -0,0 +1,301 @@ +import 'dart:convert'; + +import 'package:pinenacl/x25519.dart'; +import 'package:solana/base58.dart'; +import 'package:solana_web3/solana_web3.dart' as web3; + +/// flutter_phantom that allows users to connect to Phantom Wallet +/// This package is written from the "deep-link-demo-app(react native)" example +/// in the [deep-link-demo-app](https://github.com/phantom-labs/deep-link-demo-app) repository +class FlutterPhantom { + static const String scheme = "https"; + static const String host = "phantom.app"; + + String? _session; + + late PrivateKey _dAppSecretKey; + late PublicKey dAppPublicKey; + + /// [appUrl] is used to fetch app metadata (i.e. title, icon) using the + /// same properties found in Displaying Your App. + String appUrl; + + /// [phantomWalletPublicKey] once session is established with Phantom Wallet (i.e. user has approved the connection) we get user's Publickey. + late web3.PublicKey phantomWalletPublicKey; + + /// [deepLink] uri is used to open the app from Phantom Wallet i.e our app's deeplink. + String deepLink; + + /// [_sharedSecret] is used to encrypt and decrypt the session token and other data. + Box? _sharedSecret; + + FlutterPhantom({required this.appUrl, required this.deepLink}) { + _dAppSecretKey = PrivateKey.generate(); + dAppPublicKey = _dAppSecretKey.publicKey; + } + + Uri generateConnectUri({required String cluster, required String redirect}) { + return Uri( + scheme: scheme, + host: host, + path: '/ul/v1/connect', + queryParameters: { + 'dapp_encryption_public_key': base58encode(dAppPublicKey.toUint8List()), + 'cluster': cluster, + 'app_url': appUrl, + 'redirect_link': "$deepLink?handleQuery=$redirect", + }, + ); + } + + Map onConnectToWallet( + List>> queryParamsFromPhantom) { + String phantomPKey = queryParamsFromPhantom + .singleWhere((element) => + element.key.toString() == "phantom_encryption_public_key") + .value[0] + .toString(); + + String nonce = queryParamsFromPhantom + .singleWhere((element) => element.key.toString() == "nonce") + .value[0]; + + String data = queryParamsFromPhantom + .singleWhere((element) => element.key.toString() == "data") + .value[0]; + + Box sharedSecretDapp = Box( + myPrivateKey: _dAppSecretKey, + theirPublicKey: + PublicKey(Uint8List.fromList(base58decode(phantomPKey)))); + + _sharedSecret = sharedSecretDapp; + Map onConnectData = decryptPayload(data, nonce, sharedSecretDapp); + + phantomWalletPublicKey = + web3.PublicKey.fromString(onConnectData['public_key'].toString()); + + _session = onConnectData['session'].toString(); + return onConnectData; + } + + Uri generateSignAndSendTransactionUri( + {required web3.Buffer transaction, required String redirect}) { + var payload = { + "session": _session, + "transaction": base58encode(transaction.asUint8List()), + }; + final getData = encryptPayload(payload); + final nonce = getData[0]; + final encryptedPayload = getData[1]; + + return Uri( + scheme: scheme, + host: host, + path: '/ul/v1/signAndSendTransaction', + queryParameters: { + "dapp_encryption_public_key": base58encode(dAppPublicKey.asTypedList), + "nonce": base58encode(nonce), + 'redirect_link': "$deepLink?handleQuery=$redirect", + 'payload': base58encode(encryptedPayload) + }, + ); + } + + Map onCreateSignAndSendTransactionReceive( + List>> queryParams) { + String nonce = queryParams + .singleWhere((element) => element.key.toString() == "nonce") + .value[0]; + + String data = queryParams + .singleWhere((element) => element.key.toString() == "data") + .value[0]; + + Map onCreateAndSendTransactionResult = + decryptPayload(data, nonce, _sharedSecret!); + + return onCreateAndSendTransactionResult; + } + + web3.Transaction onSignTransactionReceive( + List>> queryParams) { + String nonce = queryParams + .singleWhere((element) => element.key.toString() == "nonce") + .value[0]; + + String data = queryParams + .singleWhere((element) => element.key.toString() == "data") + .value[0]; + + Map onSignTransactionReceiveResult = + decryptPayload(data, nonce, _sharedSecret!); + + Map transactionEncode = + onSignTransactionReceiveResult.cast(); + var transaction = base58decode(transactionEncode['transaction']); + + web3.Transaction decodedTransaction = + web3.Transaction.fromList(transaction); + return decodedTransaction; + } + + List onSignAllTransactionReceive( + List>> queryParams) { + String nonce = queryParams + .singleWhere((element) => element.key.toString() == "nonce") + .value[0]; + + String data = queryParams + .singleWhere((element) => element.key.toString() == "data") + .value[0]; + + Map onSignTransactionReceiveResult = + decryptPayload(data, nonce, _sharedSecret!); + + Map transactionsEncode = + onSignTransactionReceiveResult.cast(); + List listTransactionsEncode = + List.from(transactionsEncode['transactions'] as List); + List listTransactionsDecode = listTransactionsEncode + .map((e) => web3.Transaction.fromList(base58decode(e))) + .toList(); + + return listTransactionsDecode; + } + + String onDisconnectReceive() { + return "disConnect"; + } + + Uri generateDisconnectUri({required String redirect}) { + var payLoad = { + "session": _session, + }; + final getData = encryptPayload(payLoad); + final nonce = getData[0]; + final encryptedPayload = getData[1]; + + Uri launchUri = Uri( + scheme: scheme, + host: host, + path: '/ul/v1/disconnect', + queryParameters: { + "dapp_encryption_public_key": base58encode(dAppPublicKey.asTypedList), + "nonce": base58encode(nonce), + 'redirect_link': "$deepLink?handleQuery=$redirect", + "payload": base58encode(encryptedPayload), + }, + ); + _sharedSecret = null; + return launchUri; + } + + Uri generateSignTransactionUri( + {required String transaction, required String redirect}) { + var payload = { + "session": _session, + "transaction": transaction, + }; + final getData = encryptPayload(payload); + final nonce = getData[0]; + final encryptedPayload = getData[1]; + + return Uri( + scheme: scheme, + host: host, + path: '/ul/v1/signTransaction', + queryParameters: { + "dapp_encryption_public_key": base58encode(dAppPublicKey.asTypedList), + "nonce": base58encode(nonce), + 'redirect_link': "$deepLink?handleQuery=$redirect", + 'payload': base58encode(encryptedPayload) + }, + ); + } + + Uri generateUriSignAllTransactions( + {required List transactions, required String redirect}) { + var payload = {"session": _session, "transactions": transactions}; + final getData = encryptPayload(payload); + final nonce = getData[0]; + final encryptedPayload = getData[1]; + + return Uri( + scheme: 'https', + host: 'phantom.app', + path: '/ul/v1/signAllTransactions', + queryParameters: { + "dapp_encryption_public_key": base58encode(dAppPublicKey.asTypedList), + "nonce": base58encode(nonce), + 'redirect_link': "$deepLink?handleQuery=$redirect", + 'payload': base58encode(encryptedPayload) + }, + ); + } + + Uri generateSignMessageUri( + {required String redirect, required String message}) { + var payload = { + "session": _session, + "message": base58encode(message.codeUnits.toUint8List()), + }; + + final getData = encryptPayload(payload); + final nonce = getData[0]; + final encryptedPayload = getData[1]; + + return Uri( + scheme: scheme, + host: host, + path: 'ul/v1/signMessage', + queryParameters: { + "dapp_encryption_public_key": + base58encode(Uint8List.fromList(dAppPublicKey)), + "nonce": base58encode(nonce), + 'redirect_link': "$deepLink?handleQuery=$redirect", + 'payload': base58encode(encryptedPayload) + }, + ); + } + + Map onSignMessageReceive( + List>> queryParams) { + String nonce = queryParams + .singleWhere((element) => element.key.toString() == "nonce") + .value[0]; + + String data = queryParams + .singleWhere((element) => element.key.toString() == "data") + .value[0]; + + Map onCreateAndSendTransactionResult = + decryptPayload(data, nonce, _sharedSecret!); + + return onCreateAndSendTransactionResult; + } + + /// Decrypts the [data] payload returned by Phantom Wallet + Map decryptPayload( + String data, String nonce, Box sharedSecret) { + if (sharedSecret.isEmpty) throw ("missing shared secret"); + + Uint8List decryptedData = sharedSecret.decrypt( + ByteList(base58decode(data)), + nonce: Uint8List.fromList(base58decode(nonce)), + ); + if (decryptedData.isEmpty) throw ("Unable to decrypt data"); + return jsonDecode(String.fromCharCodes(decryptedData)); + } + + /// Encrypts the data payload to be sent to Phantom Wallet. + /// + /// - Returns the encrypted `payload` and `nonce`. + encryptPayload(payload) { + final nonce = PineNaClUtils.randombytes(24); + List list = utf8.encode(jsonEncode(payload)); + Uint8List bytes = Uint8List.fromList(list); + var encryptedPayload = _sharedSecret!.encrypt(bytes, nonce: nonce); + return [nonce, encryptedPayload]; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..2fed6c0 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,22 @@ +name: flutter_phantom +description: A new Flutter for connect phantom wallet. +version: 0.9.0 +homepage: https://github.com/NavidHosseini/flutter_phantom.git + +environment: + sdk: '>=2.19.0-171.0.dev <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + pinenacl: ^0.5.1 + solana: ^0.26.3 + solana_web3: ^0.0.2+4 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter: null diff --git a/test/flutter_phantom_test.dart b/test/flutter_phantom_test.dart new file mode 100644 index 0000000..ab73b3a --- /dev/null +++ b/test/flutter_phantom_test.dart @@ -0,0 +1 @@ +void main() {}