linkExternalWallet method
Links the connected wallet to the signed-in user and refreshes the user and session token.
Implementation
@override
Future<SdkUser> linkExternalWallet(String address) async {
Future<SdkUser> instrumentationOperation() async {
final operationScope = _state.flowScope.capture(replace: false);
await operationScope.ready();
if (!_state.initialized) {
throw DynamicStateException("call initialize() before using the client");
}
final universalLink = _config.universalLink;
if (universalLink == null) {
throw DynamicStateException("universalLink must be configured to link an external wallet");
}
final account = (_externalWalletState.externalWalletAccounts == null ? null : firstWhereOrNull<ExternalWalletAccount>(_externalWalletState.externalWalletAccounts!, (item) => item.address == address));
if (account == null) {
throw DynamicStateException("address is not a connected external wallet — call awaitExternalWalletConnection first");
}
final appName = _config.appName ?? "";
operationScope.check();
final nonceResponse = await _api.get<NoncesResponse>(path: "/nonces", token: null, sessionPublicKey: _state.sessionPublicKey, parse: NoncesResponse.fromJson, query: <String, String>{"count": "1"});
operationScope.check();
final nonce = firstOrNull(nonceResponse.nonces);
if (nonce == null) {
throw DynamicParseException("no wallet sign-in nonce returned");
}
final blockchainName = switch (account.chain) { "EVM" => "Ethereum", "SOL" => "Solana", "BTC" => "Bitcoin", "SUI" => "Sui", "COSMOS" => "Cosmos", "STARK" => "Starknet", _ => account.chain };
final chainIdLine = switch (account.chain) { "EVM" => "\nChain ID: 1", _ => "" };
final messageToSign = "${ApiClient.urlHost(universalLink)} wants you to sign in with your ${blockchainName} account:\n${address}\n\nWelcome to ${appName}. Signing is the only way we can truly know that you are the owner of the wallet you are connecting. Signing is a safe, gas-less transaction that does not in any way give ${appName} permission to perform any transactions with your wallet.\n\nURI: ${universalLink}\nVersion: 1${chainIdLine}\nNonce: ${nonce}\nIssued At: ${DateTime.now().toUtc().toIso8601String()}\nRequest ID: ${_config.environmentId}";
_state.externalWalletActionRequested.emit(ExternalWalletActionRequest(action: "signMessage", walletName: account.displayName, address: address, chain: account.chain));
operationScope.check();
final signature = await _externalWalletProvider.signMessage(account.chain, address, messageToSign);
operationScope.check();
operationScope.check();
final verify = await _api.post<VerifyResponse>(path: "/verify/link", body: <String, dynamic>{"signedMessage": signature, "messageToSign": messageToSign, "publicWalletAddress": address, "chain": account.chain, "walletName": account.verifyWalletName(), "walletProvider": "walletConnect"}, token: _state.token, sessionPublicKey: _state.sessionPublicKey, parse: VerifyResponse.fromJson);
operationScope.check();
final token = (verify.minifiedJwt ?? verify.jwt);
if (token == null) {
throw DynamicParseException("verify response contained no token");
}
final expiresAtMs = (verify.expiresAt ?? 0) * DynamicConstants.millisecondsPerSecond;
operationScope.commit(() {
_state.token = token;
_state.tokenChanged.emit(_state.token);
});
operationScope.commit(() {
_state.sessionExpiresAtMs = expiresAtMs;
_state.sessionExpiresAtMsChanged.emit(_state.sessionExpiresAtMs);
});
operationScope.commit(() {
_state.user = verify.user;
_state.userChanged.emit(_state.user);
});
operationScope.check();
await operationScope.persist(() => _secureStorage.setItem("dynamic_${_config.environmentId}_session", jsonEncode(StoredSession(token: token, sessionExpiration: expiresAtMs).toJson())));
operationScope.check();
_logger.log("info", "linked external wallet");
final signedInUser = verify.user;
if (signedInUser == null) {
throw DynamicParseException("signin response contained no user");
}
return signedInUser;
}
return await _api.instrumentation.runAsync("externalWallet.linkExternalWallet", instrumentationOperation);
}