@dubsdotapp/expo 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +68 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +759 -284
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +521 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +2 -0
- package/src/managed-wallet.tsx +150 -46
- package/src/provider.tsx +8 -0
- package/src/storage.ts +1 -0
- package/src/wallet/index.ts +2 -0
- package/src/wallet/phantom-deeplink/crypto.ts +49 -0
- package/src/wallet/phantom-deeplink/deeplink-handler.ts +177 -0
- package/src/wallet/phantom-deeplink/index.ts +2 -0
- package/src/wallet/phantom-deeplink/phantom-deeplink-adapter.ts +325 -0
package/dist/index.mjs
CHANGED
|
@@ -455,7 +455,8 @@ var DubsClient = class {
|
|
|
455
455
|
// src/storage.ts
|
|
456
456
|
var STORAGE_KEYS = {
|
|
457
457
|
MWA_AUTH_TOKEN: "dubs_mwa_auth_token",
|
|
458
|
-
JWT_TOKEN: "dubs_jwt_token"
|
|
458
|
+
JWT_TOKEN: "dubs_jwt_token",
|
|
459
|
+
PHANTOM_SESSION: "dubs_phantom_session"
|
|
459
460
|
};
|
|
460
461
|
function createSecureStoreStorage() {
|
|
461
462
|
let SecureStore = null;
|
|
@@ -493,6 +494,7 @@ import { Connection } from "@solana/web3.js";
|
|
|
493
494
|
|
|
494
495
|
// src/managed-wallet.tsx
|
|
495
496
|
import { createContext, useContext, useState, useEffect, useRef, useCallback } from "react";
|
|
497
|
+
import { Platform } from "react-native";
|
|
496
498
|
|
|
497
499
|
// src/wallet/mwa-adapter.ts
|
|
498
500
|
import { PublicKey } from "@solana/web3.js";
|
|
@@ -592,6 +594,388 @@ var MwaWalletAdapter = class {
|
|
|
592
594
|
}
|
|
593
595
|
};
|
|
594
596
|
|
|
597
|
+
// src/wallet/phantom-deeplink/phantom-deeplink-adapter.ts
|
|
598
|
+
import { PublicKey as PublicKey2, Transaction as Transaction2 } from "@solana/web3.js";
|
|
599
|
+
import bs582 from "bs58";
|
|
600
|
+
|
|
601
|
+
// src/wallet/phantom-deeplink/crypto.ts
|
|
602
|
+
import nacl from "tweetnacl";
|
|
603
|
+
import bs58 from "bs58";
|
|
604
|
+
function generateKeyPair() {
|
|
605
|
+
const kp = nacl.box.keyPair();
|
|
606
|
+
return { publicKey: kp.publicKey, secretKey: kp.secretKey };
|
|
607
|
+
}
|
|
608
|
+
function deriveSharedSecret(ourSecretKey, theirPublicKey) {
|
|
609
|
+
return nacl.box.before(theirPublicKey, ourSecretKey);
|
|
610
|
+
}
|
|
611
|
+
function encryptPayload(payload, sharedSecret) {
|
|
612
|
+
const nonce = nacl.randomBytes(24);
|
|
613
|
+
const message = new TextEncoder().encode(JSON.stringify(payload));
|
|
614
|
+
const encrypted = nacl.box.after(message, nonce, sharedSecret);
|
|
615
|
+
if (!encrypted) throw new Error("Encryption failed");
|
|
616
|
+
return {
|
|
617
|
+
nonce: bs58.encode(nonce),
|
|
618
|
+
ciphertext: bs58.encode(encrypted)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function decryptPayload(ciphertextBase58, nonceBase58, sharedSecret) {
|
|
622
|
+
const ciphertext = bs58.decode(ciphertextBase58);
|
|
623
|
+
const nonce = bs58.decode(nonceBase58);
|
|
624
|
+
const decrypted = nacl.box.open.after(ciphertext, nonce, sharedSecret);
|
|
625
|
+
if (!decrypted) throw new Error("Decryption failed \u2014 invalid shared secret or corrupted data");
|
|
626
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/wallet/phantom-deeplink/deeplink-handler.ts
|
|
630
|
+
import { Linking } from "react-native";
|
|
631
|
+
var TAG = "[Dubs:DeeplinkHandler]";
|
|
632
|
+
var DeeplinkHandler = class {
|
|
633
|
+
constructor(redirectBase) {
|
|
634
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
635
|
+
this.subscription = null;
|
|
636
|
+
this.redirectBase = redirectBase.replace(/\/+$/, "");
|
|
637
|
+
console.log(TAG, "Created with redirectBase:", this.redirectBase);
|
|
638
|
+
}
|
|
639
|
+
/** Start listening for incoming deeplinks. Call once on adapter init. */
|
|
640
|
+
start() {
|
|
641
|
+
if (this.subscription) {
|
|
642
|
+
console.log(TAG, "Already listening, skipping start()");
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
console.log(TAG, "Starting URL listener");
|
|
646
|
+
this.subscription = Linking.addEventListener("url", ({ url }) => {
|
|
647
|
+
console.log(TAG, "Received URL event:", url);
|
|
648
|
+
this.handleUrl(url);
|
|
649
|
+
});
|
|
650
|
+
Linking.getInitialURL().then((url) => {
|
|
651
|
+
if (url) {
|
|
652
|
+
console.log(TAG, "Cold-start URL found:", url);
|
|
653
|
+
this.handleUrl(url);
|
|
654
|
+
} else {
|
|
655
|
+
console.log(TAG, "No cold-start URL");
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
/** Stop listening and reject all pending requests. */
|
|
660
|
+
destroy() {
|
|
661
|
+
console.log(TAG, "Destroying \u2014 pending requests:", this.pending.size);
|
|
662
|
+
this.subscription?.remove();
|
|
663
|
+
this.subscription = null;
|
|
664
|
+
for (const [id, req] of this.pending) {
|
|
665
|
+
clearTimeout(req.timer);
|
|
666
|
+
req.reject(new Error("DeeplinkHandler destroyed"));
|
|
667
|
+
this.pending.delete(id);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Open a Phantom deeplink and wait for the redirect response.
|
|
672
|
+
* @param url The full Phantom deeplink URL to open
|
|
673
|
+
* @param requestId Unique ID embedded in the redirect_link param
|
|
674
|
+
* @param timeoutMs How long to wait before rejecting (default 120s)
|
|
675
|
+
*/
|
|
676
|
+
async send(url, requestId, timeoutMs = 12e4) {
|
|
677
|
+
console.log(TAG, `send() requestId=${requestId} timeout=${timeoutMs}ms`);
|
|
678
|
+
console.log(TAG, "Opening URL:", url.substring(0, 120) + (url.length > 120 ? "..." : ""));
|
|
679
|
+
return new Promise((resolve, reject) => {
|
|
680
|
+
const timer = setTimeout(() => {
|
|
681
|
+
console.log(TAG, `Timeout reached for requestId=${requestId}`);
|
|
682
|
+
this.pending.delete(requestId);
|
|
683
|
+
reject(new Error(`Phantom deeplink timed out after ${timeoutMs / 1e3}s`));
|
|
684
|
+
}, timeoutMs);
|
|
685
|
+
this.pending.set(requestId, { resolve, reject, timer });
|
|
686
|
+
Linking.openURL(url).catch((err) => {
|
|
687
|
+
console.log(TAG, `Failed to open URL: ${err.message}`);
|
|
688
|
+
this.pending.delete(requestId);
|
|
689
|
+
clearTimeout(timer);
|
|
690
|
+
reject(new Error(`Failed to open Phantom: ${err.message}`));
|
|
691
|
+
});
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
handleUrl(url) {
|
|
695
|
+
if (!url.startsWith(this.redirectBase)) {
|
|
696
|
+
console.log(TAG, "Ignoring URL (not our redirect base):", url.substring(0, 80));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
console.log(TAG, "Processing redirect URL:", url.substring(0, 120) + (url.length > 120 ? "..." : ""));
|
|
700
|
+
const parsed = new URL(url);
|
|
701
|
+
const params = {};
|
|
702
|
+
parsed.searchParams.forEach((value, key) => {
|
|
703
|
+
params[key] = value;
|
|
704
|
+
});
|
|
705
|
+
const paramKeys = Object.keys(params);
|
|
706
|
+
console.log(TAG, "Parsed params keys:", paramKeys.join(", "));
|
|
707
|
+
if (params.errorCode) {
|
|
708
|
+
const errorMessage = params.errorMessage ? decodeURIComponent(params.errorMessage) : `Phantom error code: ${params.errorCode}`;
|
|
709
|
+
console.log(TAG, `Phantom returned error: code=${params.errorCode} message="${errorMessage}"`);
|
|
710
|
+
const requestId2 = this.extractRequestId(url);
|
|
711
|
+
if (requestId2 && this.pending.has(requestId2)) {
|
|
712
|
+
console.log(TAG, `Routing error to requestId=${requestId2}`);
|
|
713
|
+
const req = this.pending.get(requestId2);
|
|
714
|
+
clearTimeout(req.timer);
|
|
715
|
+
this.pending.delete(requestId2);
|
|
716
|
+
req.reject(new Error(errorMessage));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
console.log(TAG, `Cannot route error to specific request, rejecting all ${this.pending.size} pending`);
|
|
720
|
+
for (const [id, req] of this.pending) {
|
|
721
|
+
clearTimeout(req.timer);
|
|
722
|
+
req.reject(new Error(errorMessage));
|
|
723
|
+
this.pending.delete(id);
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const requestId = this.extractRequestId(url);
|
|
728
|
+
console.log(TAG, `Extracted requestId=${requestId}, pending requests: [${[...this.pending.keys()].join(", ")}]`);
|
|
729
|
+
if (requestId && this.pending.has(requestId)) {
|
|
730
|
+
console.log(TAG, `Resolving requestId=${requestId}`);
|
|
731
|
+
const req = this.pending.get(requestId);
|
|
732
|
+
clearTimeout(req.timer);
|
|
733
|
+
this.pending.delete(requestId);
|
|
734
|
+
req.resolve({ params });
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const first = this.pending.entries().next().value;
|
|
738
|
+
if (first) {
|
|
739
|
+
const [id, req] = first;
|
|
740
|
+
console.log(TAG, `Fallback: resolving first pending requestId=${id}`);
|
|
741
|
+
clearTimeout(req.timer);
|
|
742
|
+
this.pending.delete(id);
|
|
743
|
+
req.resolve({ params });
|
|
744
|
+
} else {
|
|
745
|
+
console.log(TAG, "No pending requests to resolve \u2014 redirect may have arrived late");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/** Try to extract a request ID from the URL path segment after the redirect base. */
|
|
749
|
+
extractRequestId(url) {
|
|
750
|
+
const afterBase = url.slice(this.redirectBase.length);
|
|
751
|
+
const match = afterBase.match(/^\/?([a-zA-Z0-9_-]+)/);
|
|
752
|
+
return match?.[1] ?? null;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/wallet/phantom-deeplink/phantom-deeplink-adapter.ts
|
|
757
|
+
var TAG2 = "[Dubs:PhantomAdapter]";
|
|
758
|
+
var requestCounter = 0;
|
|
759
|
+
function nextRequestId() {
|
|
760
|
+
return `req${Date.now()}_${++requestCounter}`;
|
|
761
|
+
}
|
|
762
|
+
var PhantomDeeplinkAdapter = class {
|
|
763
|
+
constructor(config) {
|
|
764
|
+
this._publicKey = null;
|
|
765
|
+
this._connected = false;
|
|
766
|
+
this._dappKeyPair = null;
|
|
767
|
+
this._sharedSecret = null;
|
|
768
|
+
this._sessionToken = null;
|
|
769
|
+
this._phantomPublicKey = null;
|
|
770
|
+
console.log(TAG2, "Creating adapter with config:", {
|
|
771
|
+
redirectUri: config.redirectUri,
|
|
772
|
+
appUrl: config.appUrl,
|
|
773
|
+
cluster: config.cluster,
|
|
774
|
+
timeout: config.timeout
|
|
775
|
+
});
|
|
776
|
+
this.config = config;
|
|
777
|
+
this.timeout = config.timeout ?? 12e4;
|
|
778
|
+
this.handler = new DeeplinkHandler(config.redirectUri);
|
|
779
|
+
this.handler.start();
|
|
780
|
+
console.log(TAG2, "Adapter created and deeplink listener started");
|
|
781
|
+
}
|
|
782
|
+
get publicKey() {
|
|
783
|
+
return this._publicKey;
|
|
784
|
+
}
|
|
785
|
+
get connected() {
|
|
786
|
+
return this._connected;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Restore a previously saved session without opening Phantom.
|
|
790
|
+
* Call this before connect() if you have persisted session state.
|
|
791
|
+
*/
|
|
792
|
+
restoreSession(saved) {
|
|
793
|
+
console.log(TAG2, "Restoring session for wallet:", saved.walletPublicKey);
|
|
794
|
+
this._dappKeyPair = {
|
|
795
|
+
publicKey: bs582.decode(saved.dappPublicKey),
|
|
796
|
+
secretKey: bs582.decode(saved.dappSecretKey)
|
|
797
|
+
};
|
|
798
|
+
this._phantomPublicKey = bs582.decode(saved.phantomPublicKey);
|
|
799
|
+
this._sharedSecret = bs582.decode(saved.sharedSecret);
|
|
800
|
+
this._sessionToken = saved.sessionToken;
|
|
801
|
+
this._publicKey = new PublicKey2(saved.walletPublicKey);
|
|
802
|
+
this._connected = true;
|
|
803
|
+
console.log(TAG2, "Session restored successfully \u2014 connected:", this._publicKey.toBase58());
|
|
804
|
+
}
|
|
805
|
+
/** Serialize the current session for persistence. Returns null if not connected. */
|
|
806
|
+
getSession() {
|
|
807
|
+
if (!this._connected || !this._dappKeyPair || !this._phantomPublicKey || !this._sharedSecret || !this._sessionToken || !this._publicKey) {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
dappPublicKey: bs582.encode(this._dappKeyPair.publicKey),
|
|
812
|
+
dappSecretKey: bs582.encode(this._dappKeyPair.secretKey),
|
|
813
|
+
phantomPublicKey: bs582.encode(this._phantomPublicKey),
|
|
814
|
+
sharedSecret: bs582.encode(this._sharedSecret),
|
|
815
|
+
sessionToken: this._sessionToken,
|
|
816
|
+
walletPublicKey: this._publicKey.toBase58()
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
async connect() {
|
|
820
|
+
console.log(TAG2, "connect() \u2014 generating x25519 keypair");
|
|
821
|
+
this._dappKeyPair = generateKeyPair();
|
|
822
|
+
const dappPubBase58 = bs582.encode(this._dappKeyPair.publicKey);
|
|
823
|
+
console.log(TAG2, "Dapp public key:", dappPubBase58);
|
|
824
|
+
const requestId = nextRequestId();
|
|
825
|
+
const redirectLink = `${this.config.redirectUri}/${requestId}`;
|
|
826
|
+
console.log(TAG2, `connect() requestId=${requestId} redirectLink=${redirectLink}`);
|
|
827
|
+
const appUrl = this.config.appUrl || this.config.redirectUri;
|
|
828
|
+
console.log(TAG2, "Using app_url:", appUrl);
|
|
829
|
+
const params = new URLSearchParams({
|
|
830
|
+
dapp_encryption_public_key: dappPubBase58,
|
|
831
|
+
cluster: this.config.cluster || "mainnet-beta",
|
|
832
|
+
redirect_link: redirectLink,
|
|
833
|
+
app_url: appUrl
|
|
834
|
+
});
|
|
835
|
+
const url = `https://phantom.app/ul/v1/connect?${params.toString()}`;
|
|
836
|
+
console.log(TAG2, "Opening Phantom connect deeplink...");
|
|
837
|
+
const response = await this.handler.send(url, requestId, this.timeout);
|
|
838
|
+
console.log(TAG2, "Received connect response, param keys:", Object.keys(response.params).join(", "));
|
|
839
|
+
const phantomPubBase58 = response.params.phantom_encryption_public_key;
|
|
840
|
+
if (!phantomPubBase58) {
|
|
841
|
+
console.log(TAG2, "ERROR: No phantom_encryption_public_key in response");
|
|
842
|
+
throw new Error("Phantom did not return an encryption public key");
|
|
843
|
+
}
|
|
844
|
+
console.log(TAG2, "Phantom public key:", phantomPubBase58);
|
|
845
|
+
this._phantomPublicKey = bs582.decode(phantomPubBase58);
|
|
846
|
+
this._sharedSecret = deriveSharedSecret(
|
|
847
|
+
this._dappKeyPair.secretKey,
|
|
848
|
+
this._phantomPublicKey
|
|
849
|
+
);
|
|
850
|
+
console.log(TAG2, "Shared secret derived, decrypting response...");
|
|
851
|
+
const data = decryptPayload(
|
|
852
|
+
response.params.data,
|
|
853
|
+
response.params.nonce,
|
|
854
|
+
this._sharedSecret
|
|
855
|
+
);
|
|
856
|
+
console.log(TAG2, "Decrypted connect data \u2014 public_key:", data.public_key, "session length:", data.session?.length);
|
|
857
|
+
this._sessionToken = data.session;
|
|
858
|
+
this._publicKey = new PublicKey2(data.public_key);
|
|
859
|
+
this._connected = true;
|
|
860
|
+
console.log(TAG2, "Connected! Wallet:", this._publicKey.toBase58());
|
|
861
|
+
this.config.onSessionChange?.(this.getSession());
|
|
862
|
+
}
|
|
863
|
+
disconnect() {
|
|
864
|
+
console.log(TAG2, "disconnect() \u2014 clearing state, was connected:", this._connected, "wallet:", this._publicKey?.toBase58());
|
|
865
|
+
this._publicKey = null;
|
|
866
|
+
this._connected = false;
|
|
867
|
+
this._dappKeyPair = null;
|
|
868
|
+
this._sharedSecret = null;
|
|
869
|
+
this._sessionToken = null;
|
|
870
|
+
this._phantomPublicKey = null;
|
|
871
|
+
this.config.onSessionChange?.(null);
|
|
872
|
+
console.log(TAG2, "Disconnected");
|
|
873
|
+
}
|
|
874
|
+
async signTransaction(transaction) {
|
|
875
|
+
this.assertConnected();
|
|
876
|
+
console.log(TAG2, "signTransaction() \u2014 serializing transaction");
|
|
877
|
+
const serializedTx = bs582.encode(
|
|
878
|
+
transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
|
|
879
|
+
);
|
|
880
|
+
console.log(TAG2, "Transaction serialized, length:", serializedTx.length);
|
|
881
|
+
const { nonce, ciphertext } = encryptPayload(
|
|
882
|
+
{ transaction: serializedTx, session: this._sessionToken },
|
|
883
|
+
this._sharedSecret
|
|
884
|
+
);
|
|
885
|
+
const requestId = nextRequestId();
|
|
886
|
+
const redirectLink = `${this.config.redirectUri}/${requestId}`;
|
|
887
|
+
console.log(TAG2, `signTransaction() requestId=${requestId}`);
|
|
888
|
+
const params = new URLSearchParams({
|
|
889
|
+
dapp_encryption_public_key: bs582.encode(this._dappKeyPair.publicKey),
|
|
890
|
+
nonce,
|
|
891
|
+
payload: ciphertext,
|
|
892
|
+
redirect_link: redirectLink
|
|
893
|
+
});
|
|
894
|
+
const url = `https://phantom.app/ul/v1/signTransaction?${params.toString()}`;
|
|
895
|
+
console.log(TAG2, "Opening Phantom signTransaction deeplink...");
|
|
896
|
+
const response = await this.handler.send(url, requestId, this.timeout);
|
|
897
|
+
console.log(TAG2, "Received signTransaction response");
|
|
898
|
+
const data = decryptPayload(
|
|
899
|
+
response.params.data,
|
|
900
|
+
response.params.nonce,
|
|
901
|
+
this._sharedSecret
|
|
902
|
+
);
|
|
903
|
+
console.log(TAG2, "Decrypted signed transaction, length:", data.transaction?.length);
|
|
904
|
+
return Transaction2.from(bs582.decode(data.transaction));
|
|
905
|
+
}
|
|
906
|
+
async signAndSendTransaction(transaction) {
|
|
907
|
+
this.assertConnected();
|
|
908
|
+
console.log(TAG2, "signAndSendTransaction() \u2014 serializing transaction");
|
|
909
|
+
const serializedTx = bs582.encode(
|
|
910
|
+
transaction.serialize({ requireAllSignatures: false, verifySignatures: false })
|
|
911
|
+
);
|
|
912
|
+
console.log(TAG2, "Transaction serialized, length:", serializedTx.length);
|
|
913
|
+
const { nonce, ciphertext } = encryptPayload(
|
|
914
|
+
{ transaction: serializedTx, session: this._sessionToken },
|
|
915
|
+
this._sharedSecret
|
|
916
|
+
);
|
|
917
|
+
const requestId = nextRequestId();
|
|
918
|
+
const redirectLink = `${this.config.redirectUri}/${requestId}`;
|
|
919
|
+
console.log(TAG2, `signAndSendTransaction() requestId=${requestId}`);
|
|
920
|
+
const params = new URLSearchParams({
|
|
921
|
+
dapp_encryption_public_key: bs582.encode(this._dappKeyPair.publicKey),
|
|
922
|
+
nonce,
|
|
923
|
+
payload: ciphertext,
|
|
924
|
+
redirect_link: redirectLink
|
|
925
|
+
});
|
|
926
|
+
const url = `https://phantom.app/ul/v1/signAndSendTransaction?${params.toString()}`;
|
|
927
|
+
console.log(TAG2, "Opening Phantom signAndSendTransaction deeplink...");
|
|
928
|
+
const response = await this.handler.send(url, requestId, this.timeout);
|
|
929
|
+
console.log(TAG2, "Received signAndSendTransaction response");
|
|
930
|
+
const data = decryptPayload(
|
|
931
|
+
response.params.data,
|
|
932
|
+
response.params.nonce,
|
|
933
|
+
this._sharedSecret
|
|
934
|
+
);
|
|
935
|
+
console.log(TAG2, "Transaction sent! Signature:", data.signature);
|
|
936
|
+
return data.signature;
|
|
937
|
+
}
|
|
938
|
+
async signMessage(message) {
|
|
939
|
+
this.assertConnected();
|
|
940
|
+
console.log(TAG2, "signMessage() \u2014 message length:", message.length);
|
|
941
|
+
const { nonce, ciphertext } = encryptPayload(
|
|
942
|
+
{ message: bs582.encode(message), session: this._sessionToken },
|
|
943
|
+
this._sharedSecret
|
|
944
|
+
);
|
|
945
|
+
const requestId = nextRequestId();
|
|
946
|
+
const redirectLink = `${this.config.redirectUri}/${requestId}`;
|
|
947
|
+
console.log(TAG2, `signMessage() requestId=${requestId}`);
|
|
948
|
+
const params = new URLSearchParams({
|
|
949
|
+
dapp_encryption_public_key: bs582.encode(this._dappKeyPair.publicKey),
|
|
950
|
+
nonce,
|
|
951
|
+
payload: ciphertext,
|
|
952
|
+
redirect_link: redirectLink
|
|
953
|
+
});
|
|
954
|
+
const url = `https://phantom.app/ul/v1/signMessage?${params.toString()}`;
|
|
955
|
+
console.log(TAG2, "Opening Phantom signMessage deeplink...");
|
|
956
|
+
const response = await this.handler.send(url, requestId, this.timeout);
|
|
957
|
+
console.log(TAG2, "Received signMessage response");
|
|
958
|
+
const data = decryptPayload(
|
|
959
|
+
response.params.data,
|
|
960
|
+
response.params.nonce,
|
|
961
|
+
this._sharedSecret
|
|
962
|
+
);
|
|
963
|
+
console.log(TAG2, "Message signed, signature:", data.signature?.substring(0, 20) + "...");
|
|
964
|
+
return bs582.decode(data.signature);
|
|
965
|
+
}
|
|
966
|
+
/** Remove the Linking event listener. Call when the adapter is no longer needed. */
|
|
967
|
+
destroy() {
|
|
968
|
+
console.log(TAG2, "destroy() \u2014 cleaning up");
|
|
969
|
+
this.handler.destroy();
|
|
970
|
+
}
|
|
971
|
+
assertConnected() {
|
|
972
|
+
if (!this._connected || !this._sharedSecret || !this._sessionToken) {
|
|
973
|
+
console.log(TAG2, "assertConnected FAILED \u2014 connected:", this._connected, "hasSecret:", !!this._sharedSecret, "hasSession:", !!this._sessionToken);
|
|
974
|
+
throw new Error("Wallet not connected");
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
|
|
595
979
|
// src/ui/ConnectWalletScreen.tsx
|
|
596
980
|
import {
|
|
597
981
|
View,
|
|
@@ -770,6 +1154,7 @@ var styles = StyleSheet.create({
|
|
|
770
1154
|
|
|
771
1155
|
// src/managed-wallet.tsx
|
|
772
1156
|
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
1157
|
+
var TAG3 = "[Dubs:ManagedWallet]";
|
|
773
1158
|
var DisconnectContext = createContext(null);
|
|
774
1159
|
function useDisconnect() {
|
|
775
1160
|
return useContext(DisconnectContext);
|
|
@@ -782,85 +1167,168 @@ function ManagedWalletProvider({
|
|
|
782
1167
|
accentColor,
|
|
783
1168
|
appIcon,
|
|
784
1169
|
tagline,
|
|
1170
|
+
redirectUri,
|
|
1171
|
+
appUrl,
|
|
785
1172
|
children
|
|
786
1173
|
}) {
|
|
787
1174
|
const [connected, setConnected] = useState(false);
|
|
788
1175
|
const [connecting, setConnecting] = useState(false);
|
|
789
1176
|
const [isReady, setIsReady] = useState(false);
|
|
790
1177
|
const [error, setError] = useState(null);
|
|
1178
|
+
const usePhantom = Platform.OS === "ios" && !!redirectUri;
|
|
1179
|
+
console.log(TAG3, `Platform: ${Platform.OS}, redirectUri: ${redirectUri ? "provided" : "not set"}, usePhantom: ${usePhantom}`);
|
|
791
1180
|
const adapterRef = useRef(null);
|
|
792
1181
|
const transactRef = useRef(null);
|
|
793
1182
|
if (!adapterRef.current) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1183
|
+
if (usePhantom) {
|
|
1184
|
+
console.log(TAG3, "Creating PhantomDeeplinkAdapter");
|
|
1185
|
+
adapterRef.current = new PhantomDeeplinkAdapter({
|
|
1186
|
+
redirectUri,
|
|
1187
|
+
appUrl,
|
|
1188
|
+
cluster,
|
|
1189
|
+
onSessionChange: (session) => {
|
|
1190
|
+
if (session) {
|
|
1191
|
+
console.log(TAG3, "Phantom session changed \u2014 saving to storage, wallet:", session.walletPublicKey);
|
|
1192
|
+
storage.setItem(STORAGE_KEYS.PHANTOM_SESSION, JSON.stringify(session)).catch((err) => {
|
|
1193
|
+
console.log(TAG3, "Failed to save Phantom session:", err);
|
|
1194
|
+
});
|
|
1195
|
+
} else {
|
|
1196
|
+
console.log(TAG3, "Phantom session cleared \u2014 removing from storage");
|
|
1197
|
+
storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch((err) => {
|
|
1198
|
+
console.log(TAG3, "Failed to delete Phantom session:", err);
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
800
1201
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1202
|
+
});
|
|
1203
|
+
} else {
|
|
1204
|
+
console.log(TAG3, "Creating MwaWalletAdapter");
|
|
1205
|
+
adapterRef.current = new MwaWalletAdapter({
|
|
1206
|
+
transact: (...args) => {
|
|
1207
|
+
if (!transactRef.current) {
|
|
1208
|
+
throw new Error(
|
|
1209
|
+
"@dubsdotapp/expo: @solana-mobile/mobile-wallet-adapter-protocol-web3js is required. Install it with: npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js"
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
return transactRef.current(...args);
|
|
1213
|
+
},
|
|
1214
|
+
appIdentity: { name: appName },
|
|
1215
|
+
cluster,
|
|
1216
|
+
onAuthTokenChange: (token) => {
|
|
1217
|
+
if (token) {
|
|
1218
|
+
storage.setItem(STORAGE_KEYS.MWA_AUTH_TOKEN, token).catch(() => {
|
|
1219
|
+
});
|
|
1220
|
+
} else {
|
|
1221
|
+
storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
812
1224
|
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
815
1227
|
}
|
|
816
1228
|
const adapter = adapterRef.current;
|
|
817
1229
|
useEffect(() => {
|
|
818
1230
|
let cancelled = false;
|
|
819
1231
|
(async () => {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1232
|
+
if (usePhantom) {
|
|
1233
|
+
console.log(TAG3, "Phantom path \u2014 checking for saved session...");
|
|
1234
|
+
try {
|
|
1235
|
+
const savedJson = await storage.getItem(STORAGE_KEYS.PHANTOM_SESSION);
|
|
1236
|
+
if (savedJson && !cancelled) {
|
|
1237
|
+
console.log(TAG3, "Found saved Phantom session, restoring...");
|
|
1238
|
+
const saved = JSON.parse(savedJson);
|
|
1239
|
+
adapter.restoreSession(saved);
|
|
1240
|
+
if (!cancelled) {
|
|
1241
|
+
console.log(TAG3, "Session restored, marking connected");
|
|
1242
|
+
setConnected(true);
|
|
1243
|
+
}
|
|
1244
|
+
} else {
|
|
1245
|
+
console.log(TAG3, "No saved Phantom session found");
|
|
1246
|
+
}
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
console.log(TAG3, "Failed to restore Phantom session:", err instanceof Error ? err.message : err);
|
|
1249
|
+
} finally {
|
|
1250
|
+
if (!cancelled) {
|
|
1251
|
+
console.log(TAG3, "Phantom init complete, marking ready");
|
|
1252
|
+
setIsReady(true);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
} else {
|
|
1256
|
+
console.log(TAG3, "MWA path \u2014 dynamic-importing transact...");
|
|
1257
|
+
try {
|
|
1258
|
+
const mwa = await import("@solana-mobile/mobile-wallet-adapter-protocol-web3js");
|
|
1259
|
+
if (cancelled) return;
|
|
1260
|
+
transactRef.current = mwa.transact;
|
|
1261
|
+
console.log(TAG3, "MWA transact loaded");
|
|
1262
|
+
} catch {
|
|
1263
|
+
console.log(TAG3, "MWA not installed \u2014 transact will throw on use");
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
|
|
1267
|
+
if (savedToken && !cancelled) {
|
|
1268
|
+
console.log(TAG3, "Found saved MWA auth token, reconnecting...");
|
|
1269
|
+
adapter.setAuthToken(savedToken);
|
|
1270
|
+
await adapter.connect();
|
|
1271
|
+
if (!cancelled) {
|
|
1272
|
+
console.log(TAG3, "MWA reconnected from saved token");
|
|
1273
|
+
setConnected(true);
|
|
1274
|
+
}
|
|
1275
|
+
} else {
|
|
1276
|
+
console.log(TAG3, "No saved MWA auth token");
|
|
1277
|
+
}
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
console.log(TAG3, "MWA silent reconnect failed:", err instanceof Error ? err.message : err);
|
|
1280
|
+
} finally {
|
|
1281
|
+
if (!cancelled) {
|
|
1282
|
+
console.log(TAG3, "MWA init complete, marking ready");
|
|
1283
|
+
setIsReady(true);
|
|
1284
|
+
}
|
|
832
1285
|
}
|
|
833
|
-
} catch {
|
|
834
|
-
} finally {
|
|
835
|
-
if (!cancelled) setIsReady(true);
|
|
836
1286
|
}
|
|
837
1287
|
})();
|
|
838
1288
|
return () => {
|
|
839
1289
|
cancelled = true;
|
|
840
1290
|
};
|
|
841
|
-
}, [adapter, storage]);
|
|
1291
|
+
}, [adapter, storage, usePhantom]);
|
|
842
1292
|
const handleConnect = useCallback(async () => {
|
|
1293
|
+
console.log(TAG3, "handleConnect() \u2014 user tapped connect");
|
|
843
1294
|
setConnecting(true);
|
|
844
1295
|
setError(null);
|
|
845
1296
|
try {
|
|
846
1297
|
await adapter.connect();
|
|
1298
|
+
console.log(TAG3, "handleConnect() \u2014 success, wallet:", adapter.publicKey?.toBase58());
|
|
847
1299
|
setConnected(true);
|
|
848
1300
|
} catch (err) {
|
|
849
1301
|
const message = err instanceof Error ? err.message : "Connection failed";
|
|
1302
|
+
console.log(TAG3, "handleConnect() \u2014 failed:", message);
|
|
850
1303
|
setError(message);
|
|
851
1304
|
} finally {
|
|
852
1305
|
setConnecting(false);
|
|
853
1306
|
}
|
|
854
1307
|
}, [adapter]);
|
|
855
1308
|
const disconnect = useCallback(async () => {
|
|
856
|
-
|
|
1309
|
+
console.log(TAG3, "disconnect() \u2014 clearing all state");
|
|
1310
|
+
adapter.disconnect?.();
|
|
857
1311
|
await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {
|
|
858
1312
|
});
|
|
1313
|
+
await storage.deleteItem(STORAGE_KEYS.PHANTOM_SESSION).catch(() => {
|
|
1314
|
+
});
|
|
859
1315
|
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {
|
|
860
1316
|
});
|
|
861
1317
|
setConnected(false);
|
|
1318
|
+
console.log(TAG3, "disconnect() \u2014 done");
|
|
862
1319
|
}, [adapter, storage]);
|
|
863
|
-
|
|
1320
|
+
useEffect(() => {
|
|
1321
|
+
return () => {
|
|
1322
|
+
if (usePhantom && adapter && "destroy" in adapter) {
|
|
1323
|
+
console.log(TAG3, "Unmounting \u2014 destroying Phantom adapter");
|
|
1324
|
+
adapter.destroy();
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}, [adapter, usePhantom]);
|
|
1328
|
+
if (!isReady) {
|
|
1329
|
+
console.log(TAG3, "Not ready yet \u2014 rendering null");
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
864
1332
|
if (!connected) {
|
|
865
1333
|
if (renderConnectScreen === false) {
|
|
866
1334
|
return null;
|
|
@@ -879,6 +1347,7 @@ function ManagedWalletProvider({
|
|
|
879
1347
|
}
|
|
880
1348
|
return /* @__PURE__ */ jsx2(ConnectWalletScreen, { ...connectProps });
|
|
881
1349
|
}
|
|
1350
|
+
console.log(TAG3, "Rendering children \u2014 connected with wallet:", adapter.publicKey?.toBase58());
|
|
882
1351
|
return /* @__PURE__ */ jsx2(DisconnectContext.Provider, { value: disconnect, children: children(adapter) });
|
|
883
1352
|
}
|
|
884
1353
|
|
|
@@ -893,7 +1362,7 @@ import {
|
|
|
893
1362
|
StyleSheet as StyleSheet2,
|
|
894
1363
|
Keyboard,
|
|
895
1364
|
KeyboardAvoidingView,
|
|
896
|
-
Platform,
|
|
1365
|
+
Platform as Platform2,
|
|
897
1366
|
Image as Image2,
|
|
898
1367
|
Animated,
|
|
899
1368
|
ScrollView
|
|
@@ -1007,7 +1476,7 @@ function useNetworkGames(params) {
|
|
|
1007
1476
|
import { useState as useState6, useCallback as useCallback6 } from "react";
|
|
1008
1477
|
|
|
1009
1478
|
// src/utils/transaction.ts
|
|
1010
|
-
import { Transaction as
|
|
1479
|
+
import { Transaction as Transaction3 } from "@solana/web3.js";
|
|
1011
1480
|
async function signAndSendBase64Transaction(base64Tx, wallet) {
|
|
1012
1481
|
if (!wallet.publicKey) throw new Error("Wallet not connected");
|
|
1013
1482
|
const binaryStr = atob(base64Tx);
|
|
@@ -1015,7 +1484,7 @@ async function signAndSendBase64Transaction(base64Tx, wallet) {
|
|
|
1015
1484
|
for (let i = 0; i < binaryStr.length; i++) {
|
|
1016
1485
|
bytes[i] = binaryStr.charCodeAt(i);
|
|
1017
1486
|
}
|
|
1018
|
-
const transaction =
|
|
1487
|
+
const transaction = Transaction3.from(bytes);
|
|
1019
1488
|
if (wallet.signAndSendTransaction) {
|
|
1020
1489
|
return wallet.signAndSendTransaction(transaction);
|
|
1021
1490
|
}
|
|
@@ -1256,7 +1725,7 @@ function useCreateCustomGame() {
|
|
|
1256
1725
|
|
|
1257
1726
|
// src/hooks/useAuth.ts
|
|
1258
1727
|
import { useState as useState10, useCallback as useCallback10, useRef as useRef2, useContext as useContext2 } from "react";
|
|
1259
|
-
import
|
|
1728
|
+
import bs583 from "bs58";
|
|
1260
1729
|
|
|
1261
1730
|
// src/auth-context.ts
|
|
1262
1731
|
import { createContext as createContext2 } from "react";
|
|
@@ -1294,7 +1763,7 @@ function useAuth() {
|
|
|
1294
1763
|
setStatus("signing");
|
|
1295
1764
|
const messageBytes = new TextEncoder().encode(message);
|
|
1296
1765
|
const signatureBytes = await wallet.signMessage(messageBytes);
|
|
1297
|
-
const signature =
|
|
1766
|
+
const signature = bs583.encode(signatureBytes);
|
|
1298
1767
|
setStatus("verifying");
|
|
1299
1768
|
const result = await client.authenticate({ walletAddress, signature, nonce });
|
|
1300
1769
|
if (result.needsRegistration) {
|
|
@@ -1795,7 +2264,7 @@ function DefaultRegistrationScreen({
|
|
|
1795
2264
|
KeyboardAvoidingView,
|
|
1796
2265
|
{
|
|
1797
2266
|
style: [s.container, { backgroundColor: t.background }],
|
|
1798
|
-
behavior:
|
|
2267
|
+
behavior: Platform2.OS === "ios" ? "padding" : void 0,
|
|
1799
2268
|
children: /* @__PURE__ */ jsx3(
|
|
1800
2269
|
ScrollView,
|
|
1801
2270
|
{
|
|
@@ -1896,7 +2365,9 @@ function DubsProvider({
|
|
|
1896
2365
|
renderLoading,
|
|
1897
2366
|
renderError,
|
|
1898
2367
|
renderRegistration,
|
|
1899
|
-
managed = true
|
|
2368
|
+
managed = true,
|
|
2369
|
+
redirectUri,
|
|
2370
|
+
appUrl
|
|
1900
2371
|
}) {
|
|
1901
2372
|
const config = NETWORK_CONFIG[network];
|
|
1902
2373
|
const baseUrl = baseUrlOverride || config.baseUrl;
|
|
@@ -1946,6 +2417,8 @@ function DubsProvider({
|
|
|
1946
2417
|
accentColor: uiConfig.accentColor,
|
|
1947
2418
|
appIcon: uiConfig.appIcon,
|
|
1948
2419
|
tagline: uiConfig.tagline,
|
|
2420
|
+
redirectUri,
|
|
2421
|
+
appUrl,
|
|
1949
2422
|
children: (adapter) => /* @__PURE__ */ jsx4(
|
|
1950
2423
|
ManagedInner,
|
|
1951
2424
|
{
|
|
@@ -2765,7 +3238,7 @@ import {
|
|
|
2765
3238
|
Animated as Animated2,
|
|
2766
3239
|
StyleSheet as StyleSheet10,
|
|
2767
3240
|
KeyboardAvoidingView as KeyboardAvoidingView2,
|
|
2768
|
-
Platform as
|
|
3241
|
+
Platform as Platform3
|
|
2769
3242
|
} from "react-native";
|
|
2770
3243
|
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2771
3244
|
var STATUS_LABELS2 = {
|
|
@@ -2877,7 +3350,7 @@ function CreateCustomGameSheet({
|
|
|
2877
3350
|
KeyboardAvoidingView2,
|
|
2878
3351
|
{
|
|
2879
3352
|
style: styles9.keyboardView,
|
|
2880
|
-
behavior:
|
|
3353
|
+
behavior: Platform3.OS === "ios" ? "padding" : void 0,
|
|
2881
3354
|
children: /* @__PURE__ */ jsx12(View10, { style: styles9.sheetPositioner, children: /* @__PURE__ */ jsxs10(View10, { style: [styles9.sheet, { backgroundColor: t.background }], children: [
|
|
2882
3355
|
/* @__PURE__ */ jsx12(View10, { style: styles9.handleRow, children: /* @__PURE__ */ jsx12(View10, { style: [styles9.handle, { backgroundColor: t.textMuted }] }) }),
|
|
2883
3356
|
/* @__PURE__ */ jsxs10(View10, { style: styles9.header, children: [
|
|
@@ -3129,6 +3602,7 @@ export {
|
|
|
3129
3602
|
LivePoolsCard,
|
|
3130
3603
|
MwaWalletAdapter,
|
|
3131
3604
|
NETWORK_CONFIG,
|
|
3605
|
+
PhantomDeeplinkAdapter,
|
|
3132
3606
|
PickWinnerCard,
|
|
3133
3607
|
PlayersCard,
|
|
3134
3608
|
SOLANA_PROGRAM_ERRORS,
|