magiclabs / magic-js

Magic browser/React Native JavaScript SDK is your entry-point to integrating passwordless authentication inside your application.
https://magic.link/docs/api-reference/client-side-sdks/web
Apache License 2.0
463 stars 86 forks source link

Possible issue with Relayer initialization #610

Closed SergeyYuhimovich closed 10 months ago

SergeyYuhimovich commented 1 year ago

โœ… Prerequisites

๐Ÿ› Description

We're currently working on the refactoring of our networks switching functionality and we've found that there could be a possible issue with initialization of the Relayer, which leads to magic or web3 async methods neither resolve or reject (stuck indefinitely) if being executed right after Relayer re-render.

๐Ÿงฉ Steps to Reproduce

  1. We're using @magic-sdk/react-native-bare, tRPC + React Query for requests, Zustand for state management.
  2. Every request has async headers which got token via async function that uses magic to get user info and web3 to create signature. Magic instances for every supported network (Ethereum, BSC, Polygon) are being stored in Map in Zustand store, as well as currently selected network.
  3. Every time we change network, we re-render magic relayer with relevant settings, and our queries being automatically requested to receive data for this particular network from our BE.
  4. Switch network-1 -> network-2 -> network-1 OR network-1 -> network-2 -> network-3 -> network-1/network-2.

๐Ÿค” Expected behaviour

Changing selected network leads to re-render of <magic.Relayer /> with correct settings, magic and web3 async methods which are executed right after are being resolved.

๐Ÿ˜ฎ Actual behavior

Async function that generates token for headers being stuck indefinitely either on magic.user.getInfo() or web3.eth.personal.sign() methods. We was able to "fix" it by adding promise in the start of generateToken() function that would delay it's execution for couple of seconds. In that case, magic's and web3's methods will work as expected.

๐Ÿ’ป Code Sample

App.tsx

function App(): React.ReactElement {
  return (
    <SafeAreaProvider>
      <trpc.Provider client={trpcClient} queryClient={queryClient}>
        <QueryClientProvider client={queryClient}>
          <View>
            <MagicRelayer />
            <Navigation />
          </View>
        </QueryClientProvider>
      </trpc.Provider>
    </SafeAreaProvider>
  );
}

MagicRelayer.tsx

export const MagicRelayer = () => {
  const currentNetwork = useStore(state => state.currentNetwork);

  const magicMap = useStore(state => state.magicMap);
  const magic = magicMap.get(currentNetwork?.networkCode ?? '1');

  return <magic.Relayer />;
};

client.ts

export const trpcClient = trpc.createClient({
  links: [
    httpLink({
      url: Config.TRPC_URL,
      async headers() {
        const token = await generateToken();

        return {
          authorization: `web3 ${token}`,
        };
      },
    }),
  ],
  transformer: {
    serialize: SuperJSON.stringify,
    deserialize: SuperJSON.parse,
  },
});

generateToken.ts

export const generateToken = async () => {
  const currentNetwork = useStore.getState().currentNetwork;
  const magicMap = useStore.getState().magicMap;

  const magic = magicMap.get(currentNetwork?.networkCode ?? '1');

  const userInfo = await magic.user.getInfo(); // <- HERE we're stuck indefinite

  const message = JSON.stringify({
    email: userInfo.email,
    until: new Date(Date.now() + 2 * 60 * 1000).toISOString(),
  });

  const signature = await web3.eth.personal
    .sign(message, userInfo.publicAddress, ''); // <- OR HERE, if we decide to save user data in store and not to execute magic.user.getInfo()

  return [userInfo.publicAddress ?? '', message, signature]
    .map(decoded => Buffer.from(decoded).toString('base64'))
    .join('.');
};

createNetowrkSlice.ts - a slice in Zustand store where we store our magic instances, network info, and change current network.

const magic = new Magic(Config.MAGIC_KEY, {network: 'mainnet'});
const magicMap = new Map().set('1', magic);

const initialState: State = {
  currentNetwork: undefined,
  magicMap,
};

export const createNetworkSlice: StateCreator<NetworkSlice> = set => ({
  ...initialState,
  setMagicMap: newMagicMap =>
    set(() => ({
      magicMap: newMagicMap,
    })),
  setCurrentNetwork: network =>
    set(() => {
      return {
        currentNetwork: network,
      };
    }),
});

Home.tsx

export function Home(props: Props): React.ReactElement {
  const currentNetwork = useStore(state => state.currentNetwork);
  const setCurrentNetwork = useStore(state => state.setCurrentNetwork);

  const {
    data: balance,
  } = trpc.app.balance.get.useQuery(
    {
      networkId: currentNetwork?.networkCode ?? NetworkCode.eth,
    },
    {
      enabled: Boolean(currentNetwork),
    },
  );

  const {data: networks} =
    trpc.app.network.getNetworks.useQuery();

  useEffect(() => {
    if (!currentNetwork && networks) {
      const magicMapTemp = new Map();

      networks.forEach(n =>
        magicMapTemp.set(
          n.networkCode,
          new Magic(Config.MAGIC_KEY, {
            network: {
              rpcUrl: n.rpcUrl,
              chainId: Number(n.networkCode),
            },
          }),
        ),
      );

      setMagicMap(magicMapTemp);
      setCurrentNetwork(networks[0]);
    }
  }, [currentNetwork, networks, setCurrentNetwork, setMagicMap]);

  const _onSwitchNetworkPress = (network: Network) => {
    setCurrentNetwork(network);
  };

  return (
    <View style={{paddingHorizontal: 16}}>
      {
        networks?.map(n => (
          <Pressable
            onPress={() => _onSwitchNetworkPress(n)}
            key={n.networkCode}>
            <Text>Switch to {n.name}</Text>
          </Pressable>
        ))
      )}
    </View>
  );
}

๐ŸŒŽ Environment

Software Version(s)
magic-sdk 20.3.0
Browser -
yarn 1.22.19
Operating System macOS Ventura 13.4 + iOS 16.2 Simulator
romin-halltari commented 10 months ago

This has been fixed on the latest version of the library. Can you install the latest version and let us know if you're still encountering this issue?

SergeyYuhimovich commented 10 months ago

@romin-halltari I tested today with "@magic-sdk/react-native-bare": "22.3.4", and unfortunately this issue is still there. Same behavior: magic's async methods executed right after switch from network1 to network2 going well, switch back from network2 to network1 results in infinite await of magic.user.getInfo() to resolve.

romin-halltari commented 10 months ago

Hi @SergeyYuhimovich! I ran a little POC locally to check if there's any issues when switching between magic instances and rerendering the relayer. I am creating 2 magic instances (one for each network), and I'm calling getInfo every time I switch to the other instance. This test ran fine for me though, which tells me you might have a race condition in your code. That would also explain the fact that when you set a timeout, the call to getInfo resolves. I would also recommend trying to not save the Magic instance on your app state. Instead, save the relevant info that would help you create a Magic instance the moment you need to switch to another network.

export default function App() {
  const [network1Enabled, setNetwork1Enabled] = useState(true)

  // first network magic instance
  const magic1 = new Magic('pk_live_...', {
    extensions: [
        // extension for network 1
    ],
  });

  // second network magic instance
  const magic2 = new Magic('pk_live..., {
    extensions: [
      // extension for network 2
    ],
  });

  useEffect(() => {
    fetchInfo()
  }, [network1Enabled]);

  const fetchInfo = async () => {
    if (network1Enabled) {
      alert(JSON.stringify(await magic1.user.getInfo()))
    } else {
      alert(JSON.stringify(await magic2.user.getInfo()))
    }
  }

  return (
      <SafeAreaView>
        <Text onPress={() => {
          setNetwork1Enabled(!network1Enabled)
        }}>Switch enabled magic instance</Text>
        {network1Enabled ? <magic1.Relayer /> : <magic2.Relayer/>}
      </SafeAreaView>
  )
}