Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 2.8.0
- Convert to JS interop

## 2.7.6
- Update deps

Expand Down
110 changes: 36 additions & 74 deletions example/metamask/web/main.dart
Original file line number Diff line number Diff line change
@@ -1,74 +1,29 @@
import 'dart:convert';
import 'dart:html';
import 'dart:js_interop';
import 'dart:js_util' as js_util;
import 'dart:typed_data';

import 'package:js/js.dart'
if (dart.library.io) 'package:webthree/src/browser/js_stub.dart'
if (dart.library.js) 'package:js/js.dart';
import 'package:js/js_util.dart'
if (dart.library.io) 'package:webthree/src/browser/js_util_stub.dart'
if (dart.library.js) 'package:js/js_util.dart';
import 'package:web/web.dart' as web;
import 'package:webthree/browser.dart';
import 'package:webthree/webthree.dart';

//Javascript object conversion
Object mapToJsObject(Map map) {
final object = newObject();
map.forEach((k, v) {
if (v is Map) {
setProperty(object, k, mapToJsObject(v));
} else {
setProperty(object, k, v);
}
});
return object;
}

Map jsObjectToMap(dynamic jsObject) {
final Map result = {};
final List keys = _objectKeys(jsObject);
for (final dynamic key in keys) {
final dynamic value = getProperty(jsObject, key);
List nestedKeys = [];
if (value is List) {
nestedKeys = objectKeys(value);
}
if (nestedKeys.isNotEmpty) {
//nested property
result[key] = jsObjectToMap(value);
} else {
result[key] = value;
}
}
return result;
}

List<String> objectKeys(dynamic jsObject) {
return _objectKeys(jsObject);
}

@JS('Object.keys')
external List<String> _objectKeys(jsObject);

@JS()
@anonymous
class JSrawRequestSwitchChainParams {
external String get chainId;
class _SwitchChainParams {
external JSString get chainId;

// Must have an unnamed factory constructor with named arguments.
external factory JSrawRequestSwitchChainParams({String chainId});
external factory _SwitchChainParams({JSString chainId});
}

@JS('JSON.stringify')
external String stringify(Object obj);
//javascript object conversion ends
external String stringify(JSAny? obj);

Future<void> main() async {
await metamask();
}

Future<void> metamask() async {
final eth = window.ethereum;
final eth = web.window.ethereum;
if (eth == null) {
print('MetaMask is not available');
return;
Expand All @@ -78,7 +33,7 @@ Future<void> metamask() async {
final client = Web3Client.custom(eth.asRpcService());
final credentials = await eth.requestAccounts();

print('Using ${credentials[0].address}');
print('Using ${credentials[0].address.hex}');
print('Client is listening: ${await client.isListeningForNetwork()}');

final message = Uint8List.fromList(utf8.encode('Hello from webthree'));
Expand All @@ -87,7 +42,7 @@ Future<void> metamask() async {
}

Future<void> binanceChainWallet() async {
final bsc = window.BinanceChain;
final bsc = web.window.BinanceChain;
if (bsc == null) {
print('BinanceWallet is not available');
return;
Expand All @@ -96,7 +51,7 @@ Future<void> binanceChainWallet() async {
final client = Web3Client.custom(bsc.asRpcService());
final credentials = await bsc.requestAccounts();

print('Using ${credentials[0].address}');
print('Using ${credentials[0].address.hex}');
print('Client is listening: ${await client.isListeningForNetwork()}');

final message = Uint8List.fromList(utf8.encode('Hello from webthree'));
Expand All @@ -106,7 +61,7 @@ Future<void> binanceChainWallet() async {
}

Future<void> okxWallet() async {
final okx = window.OkxChainWallet;
final okx = web.window.OkxChainWallet;
if (okx == null) {
print('OkxChainWallet is not available');
return;
Expand All @@ -115,7 +70,7 @@ Future<void> okxWallet() async {
final client = Web3Client.custom(okx.asRpcService());
final credentials = await okx.requestAccounts();

print('Using ${credentials[0].address}');
print('Using ${credentials[0].address.hex}');
print('Client is listening: ${await client.isListeningForNetwork()}');

final message = Uint8List.fromList(utf8.encode('Hello from webthree'));
Expand All @@ -125,8 +80,7 @@ Future<void> okxWallet() async {
}

Future<void> addChain() async {
//must assign eth object in function, otherwise rawRequest is not available
final eth = window.ethereum;
final eth = web.window.ethereum;
if (eth == null) {
print('Wallet is not available');
return;
Expand All @@ -145,32 +99,40 @@ Future<void> addChain() async {
],
'iconUrls': [''],
};
await eth
.rawRequest('wallet_addEthereumChain', params: [mapToJsObject(params)]);

try {
await js_util.promiseToFuture(eth.rawRequest('wallet_addEthereumChain',
params: js_util.jsify([params]) as JSObject?));
print('Dodao network added');
} on Object catch (e) {
print('Failed to add Dodao network: $e');
}
}

Future<void> switchChain() async {
//must assign eth object in function, otherwise rawRequest is not available
final eth = window.ethereum;
final eth = web.window.ethereum;
if (eth == null) {
print('Wallet is not available');
return;
}
try {
final chainIdHex = await eth.rawRequest('eth_chainId');
print('current chain id $chainIdHex');
} on EthereumException catch (e) {
print('user rejected ${e.message}');
}

try {
await eth.rawRequest('wallet_switchEthereumChain',
params: [JSrawRequestSwitchChainParams(chainId: '0xd0da0')]);
final chainIdResult =
await js_util.promiseToFuture<String>(eth.rawRequest('eth_chainId'));
print('Current chain id $chainIdResult');

await js_util.promiseToFuture(eth.rawRequest('wallet_switchEthereumChain',
params: js_util.jsify([_SwitchChainParams(chainId: '0xd0da0'.toJS)])
as JSObject?));
print('Switched to Dodao network');
} on EthereumException catch (e) {
print('EthereumException during switchChain: ${e.code} ${e.message}');
if (e.code == 4902) {
print('Network not found, attempting to add...');
await addChain();
} else {
print('user rejected ${e.message}');
print('User rejected switch or other error');
}
} catch (e) {
print('Generic error during switchChain: $e');
}
}
33 changes: 23 additions & 10 deletions lib/src/browser/binance_wallet/credentials.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
@JS()
library webthree.internal.js.creds;

import 'dart:js_interop';
import 'dart:js_util';
import 'dart:typed_data';

import 'package:js/js.dart';
import 'package:webthree/webthree.dart';

import '../../../crypto.dart';

import 'dart_wrappers.dart';
import 'javascript.dart';

class BinanceWalletCredentials extends CredentialsWithKnownAddress
Expand All @@ -28,10 +26,19 @@ class BinanceWalletCredentials extends CredentialsWithKnownAddress

@override
Future<Uint8List> signPersonalMessage(Uint8List payload, {int? chainId}) {
return bsc.rawRequest('eth_sign', params: [
final paramsValue = jsify([
address.hex,
_bytesToData(payload),
]).then(_responseToBytes);
]);

final responsePromise = bsc.request(RequestArguments(
method: 'eth_sign',
params: paramsValue as JSAny?,
));

return (responsePromise as JSPromise<JSString>)
.toDart
.then((JSString jsResult) => _responseToBytes(jsResult.toDart));
}

@override
Expand All @@ -45,10 +52,16 @@ class BinanceWalletCredentials extends CredentialsWithKnownAddress
data: _bytesToData(transaction.data),
);

return bsc.rawRequest(
'eth_sendTransaction',
params: [param],
).then((res) => res as String);
final paramsValue = jsify([param]);

final responsePromise = bsc.request(RequestArguments(
method: 'eth_sendTransaction',
params: paramsValue as JSAny?,
));

return (responsePromise as JSPromise<JSString>)
.toDart
.then((JSString jsResult) => jsResult.toDart);
}
}

Expand Down
19 changes: 11 additions & 8 deletions lib/src/browser/binance_wallet/dart_wrappers.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_util';

import 'package:js/js_util.dart';
import 'package:webthree/src/core/exception_utils_js.dart'
if (dart.library.io) 'package:webthree/src/core/exception_utils_io.dart'
if (dart.library.js) 'package:webthree/src/core/exception_utils_js.dart';
Expand Down Expand Up @@ -41,13 +42,14 @@ extension DartBinanceChain on BinanceChainWallet {
///
/// See also:
/// - the rpc documentation under https://binance-wallet.gitbook.io/binance-chain-wallet/dev/get-started
Future<dynamic> rawRequest(String method, {Object? params}) {
// No, this can't be simplified. Binance Wallet wants `params` to be undefined.
Future<dynamic> rawRequest(String method, {JSAny? params}) {
final args = params == null
? RequestArguments(method: method)
: RequestArguments(method: method, params: params);
return promiseToFuture(request(args)).onError((error, stackTrace) {
final jsPromise = request(args);
return jsPromise.toDart.catchError((error, stackTrace) {
ExceptionUtils.analyzeException(error!);
throw error;
});
}

Expand Down Expand Up @@ -96,7 +98,8 @@ class _BinanceWalletRpcService extends RpcService {

@override
Future<RPCResponse> call(String function, [List? params]) {
return _binancechain.rawRequest(function, params: params).then((res) {
final jsParams = params != null ? jsify(params) as JSAny? : null;
return _binancechain.rawRequest(function, params: jsParams).then((res) {
return RPCResponse(0, res);
});
}
Expand Down Expand Up @@ -208,15 +211,15 @@ class _EventStreamSubscription implements StreamSubscription<dynamic> {

void _resumeIfNecessary() {
if (_onData != null && !isPaused) {
final cb = _jsCallback = allowInterop(_onData!);
_client.on(_eventName, cb);
final cb = _jsCallback = _onData!;
_client.on(_eventName, cb as JSFunction);
}
}

void _stopListening() {
final callback = _jsCallback;
if (callback != null) {
_client.removeListener(_eventName, callback);
_client.removeListener(_eventName, callback as JSFunction);
}
}
}
27 changes: 14 additions & 13 deletions lib/src/browser/binance_wallet/javascript.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
@JS()
library webthree.internal.js;

import 'dart:html';
import 'dart:js_interop';

import 'package:js/js.dart';
import 'package:meta/meta.dart';

import 'dart_wrappers.dart';
import 'package:web/web.dart' as web;

@JS('BinanceChain')
external BinanceChainWallet? get _binanceChain;

/// Extension to load obtain the `BinanceChain` window property injected by
/// BinanceChain browser plugins.
extension GetBinanceChain on Window {
extension GetBinanceChain on web.Window {
/// Loads the ethereum instance provided by the browser.
///
/// For more information on how to use this object with the webthree package,
Expand All @@ -23,30 +20,34 @@ extension GetBinanceChain on Window {
}

@JS()
class BinanceChainWallet {
@staticInterop
class BinanceChainWallet {}

extension BinanceChainWalletExtension on BinanceChainWallet {
external int get chainId;
external bool autoRefreshOnNetworkChange;
external bool get autoRefreshOnNetworkChange;
external set autoRefreshOnNetworkChange(bool value);
external bool isConnected();

/// This should not be used in user code. Use `stream(event)` instead.
@internal
external void on(String event, Function callback);
external void on(String event, JSFunction? callback);

/// This should not be used in user code. Use `stream(event)` instead.
@internal
external void removeListener(String event, Function callback);
external void removeListener(String event, JSFunction? callback);

/// This should not be used in user code. Use `requestRaw` instead.
@internal
external Object request(RequestArguments args);
external JSPromise request(RequestArguments args);
}

@JS()
@anonymous
@internal
class RequestArguments {
external String get method;
external Object? get params;
external JSAny? get params;

external factory RequestArguments({required String method, Object? params});
external factory RequestArguments({String method, JSAny? params});
}
Loading