Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for Torus #97

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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 packages/use-solana/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
"@project-serum/sol-wallet-adapter": "^0.2.0",
"@saberhq/solana": "^0.6.2",
"@solana/web3.js": "^1.19.0",
"@toruslabs/fetch-node-details": "^2.7.0",
"@toruslabs/torus.js": "^2.4.1",
"@types/bs58": "^4.0.1",
"bs58": "^4.0.1",
"eventemitter3": "^4.0.7",
"react-google-login": "^5.2.2",
"tslib": "^2.3.0",
"unstated-next": "^1.1.0"
},
Expand Down
194 changes: 194 additions & 0 deletions packages/use-solana/src/adapters/torus/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { Keypair } from "@solana/web3.js";
import NodeDetailsManager, {
INodeDetails,
} from "@toruslabs/fetch-node-details";
import Torus from "@toruslabs/torus.js";
import * as React from "react";
import {
GoogleLoginResponse,
GoogleLoginResponseOffline,
useGoogleLogin,
} from "react-google-login";
import nacl from "tweetnacl";

const origin = window.location.origin;
const USE_TORUS_TESTNET = origin === "http://localhost:3000";

// Torus is only enabled for authorized domains
const ENABLE_TORUS =
USE_TORUS_TESTNET ||
origin === "https://break.solana.com" ||
origin === "https://staging.break.solana.com";

const CLIENT_ID =
"785716588020-b5a4fheugq38c23do3p2l73iumfrklnr.apps.googleusercontent.com";
const TEST_CLIENT_ID =
"785716588020-p8kdid1dltqsafcl23g82fb9funikaj7.apps.googleusercontent.com";
const VERIFIER = "breaksolana-google";

const NODE_DETAILS = USE_TORUS_TESTNET
? new NodeDetailsManager({
network: "ropsten",
proxyAddress: "0x4023d2a0D330bF11426B12C6144Cfb96B7fa6183",
})
: new NodeDetailsManager();

type ConnectionMode = "cached" | "fresh";
type FetchStatus = "fetching" | "reloading" | "success" | "failure";

export type TorusState = {
enabled: boolean;
error?: string;
loaded: boolean;
loadingWallet: boolean;
email?: string;
wallet?: Keypair;
connect: (mode: ConnectionMode) => void;
disconnect: () => void;
};

export const useTorusState = ({
onError = (err, message) => console.error(`[Torus error]: ${message}`, err),
}: {
onError?: (err: Error, message: string) => void;
}): TorusState => {
const [fetchStatus, setFetchStatus] = React.useState<FetchStatus>();
const [googleResponse, setGoogleResponse] =
React.useState<GoogleLoginResponse>();
const [nodeDetails, setNodeDetails] = React.useState<INodeDetails | null>(
null
);
const [wallet, setWallet] = React.useState<Keypair>();
const [error, setError] = React.useState<string>();

const responseGoogle = React.useCallback(
(response: GoogleLoginResponse | GoogleLoginResponseOffline) => {
if (!("code" in response)) {
setGoogleResponse(response);
}
},
[]
);

const { signIn, loaded } = useGoogleLogin({
clientId: USE_TORUS_TESTNET ? TEST_CLIENT_ID : CLIENT_ID,
onSuccess: responseGoogle,
onFailure: (err) => {
if (!ENABLE_TORUS) return;
onError(err, "Google login failed");
setFetchStatus("failure");
setError("Failed to login");
},
isSignedIn: ENABLE_TORUS,
});

const disconnect = React.useCallback(() => {
if (!googleResponse) return;
googleResponse.disconnect();
setGoogleResponse(undefined);
}, [googleResponse]);

const connect = React.useCallback(
(mode: ConnectionMode) => {
if (mode === "fresh") {
setFetchStatus("fetching");
disconnect();
signIn();
} else if (mode === "cached") {
setFetchStatus("reloading");
}
},
[disconnect, signIn]
);

React.useEffect(() => {
if (!ENABLE_TORUS) return;

let unmounted = false;
NODE_DETAILS.getNodeDetails()
.then((details) => {
!unmounted && setNodeDetails(details);
})
.catch((err) => {
onError(err, "Fetching torus node details");
});

return () => {
unmounted = true;
};
}, []);

// Detects when a user has signed in and fetches the user's Torus private key
React.useEffect(() => {
if (!nodeDetails || !googleResponse) return;

let connectionMode: ConnectionMode;
if (fetchStatus === "reloading") {
connectionMode = "cached";
} else if (fetchStatus === "fetching") {
connectionMode = "fresh";
} else {
return;
}

let unmounted = false;
void (async () => {
const torus = new Torus({});
const { torusNodeEndpoints, torusNodePub, torusIndexes } = nodeDetails;

try {
const verifierId = googleResponse.getBasicProfile().getEmail();

// Creates a new key for the verifierId if it doesn't exist yet
await torus.getPublicAddress(
torusNodeEndpoints,
torusNodePub,
{ verifier: VERIFIER, verifierId },
false
);

let idToken = googleResponse.getAuthResponse().id_token;
if (connectionMode === "cached") {
idToken = (await googleResponse.reloadAuthResponse()).id_token;
}

const { privKey } = await torus.retrieveShares(
torusNodeEndpoints,
torusIndexes,
VERIFIER,
{ verifier_id: verifierId },
idToken
);
if (unmounted) return;
const torusKey = Buffer.from(privKey.toString(), "hex");
const keyPair = nacl.sign.keyPair.fromSeed(torusKey);
setWallet(Keypair.fromSecretKey(keyPair.secretKey));
setFetchStatus("success");
} catch (err) {
onError(err, "failed to fetch torus key");
setFetchStatus("failure");
setError("Failed to fetch Torus key");
}
})();

return () => {
unmounted = true;
};
}, [nodeDetails, googleResponse, fetchStatus, onError]);

const state = React.useMemo(
() => ({
enabled: ENABLE_TORUS,
loaded: loaded || !ENABLE_TORUS,
loadingWallet: fetchStatus === "fetching" || fetchStatus === "reloading",
error,
wallet,
email: googleResponse?.getBasicProfile().getEmail(),
connect,
disconnect,
}),
[loaded, fetchStatus, error, wallet, googleResponse, connect, disconnect]
);

return state;
};
Loading