From 56522525268c2d7024505188a43a58e025b97b56 Mon Sep 17 00:00:00 2001 From: Altynbek Orumbayev Date: Tue, 12 Dec 2023 00:15:12 +0100 Subject: [PATCH 01/10] feat: introducing magic auth provider --- package.json | 14 +- src/clients/base/base.ts | 2 +- src/clients/index.ts | 3 + src/clients/magic/client.ts | 244 +++++++++ src/clients/magic/constants.ts | 3 + src/clients/magic/index.ts | 3 + src/clients/magic/types.ts | 33 ++ src/components/Example/Connect.tsx | 23 +- src/components/Example/Example.tsx | 11 + src/constants/constants.ts | 3 +- src/hooks/useInitializeProviders.ts | 13 +- src/hooks/useWallet.ts | 12 +- src/testUtils/mockClients.ts | 52 +- src/types/providers.ts | 8 + src/types/wallet.ts | 2 +- yarn.lock | 744 +++++++++++++++++++++++++++- 16 files changed, 1145 insertions(+), 25 deletions(-) create mode 100644 src/clients/magic/client.ts create mode 100644 src/clients/magic/constants.ts create mode 100644 src/clients/magic/index.ts create mode 100644 src/clients/magic/types.ts diff --git a/package.json b/package.json index 4cbcfeb9..71f56be3 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,9 @@ "ts-node": "^10.9.1", "tsconfig-paths-webpack-plugin": "^4.0.1", "tslib": "^2.4.0", - "typescript": "^4.8.4" + "typescript": "^4.8.4", + "@magic-ext/algorand": "^16.3.2", + "magic-sdk": "^21.3.1" }, "peerDependencies": { "@blockshake/defly-connect": "^1.1.5", @@ -103,9 +105,17 @@ "@randlabs/myalgo-connect": "^1.4.2", "algosdk": "^2.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "@magic-ext/algorand": "^16.3.2", + "magic-sdk": "^21.3.1" }, "peerDependenciesMeta": { + "@magic-ext/algorand": { + "optional": true + }, + "@magic-sdk": { + "optional": true + }, "@blockshake/defly-connect": { "optional": true }, diff --git a/src/clients/base/base.ts b/src/clients/base/base.ts index a9e1e6de..643a3b63 100644 --- a/src/clients/base/base.ts +++ b/src/clients/base/base.ts @@ -24,7 +24,7 @@ abstract class BaseClient { static metadata: Metadata - abstract connect(onDisconnect: () => void): Promise + abstract connect(onDisconnect: () => void, email?: string): Promise abstract disconnect(): Promise abstract reconnect(onDisconnect: () => void): Promise abstract signTransactions( diff --git a/src/clients/index.ts b/src/clients/index.ts index 0df3d957..9b6cbdd2 100644 --- a/src/clients/index.ts +++ b/src/clients/index.ts @@ -5,6 +5,7 @@ import defly from './defly' import exodus from './exodus' import algosigner from './algosigner' import walletconnect from './walletconnect2' +import magic from './magic' import kmd from './kmd' import mnemonic from './mnemonic' import { CustomProvider } from './custom/types' @@ -20,6 +21,7 @@ export { kmd, mnemonic, custom, + magic, CustomProvider } @@ -33,5 +35,6 @@ export default { [walletconnect.metadata.id]: walletconnect, [kmd.metadata.id]: kmd, [mnemonic.metadata.id]: mnemonic, + [magic.metadata.id]: magic, [custom.metadata.id]: custom } diff --git a/src/clients/magic/client.ts b/src/clients/magic/client.ts new file mode 100644 index 00000000..e8a0adcd --- /dev/null +++ b/src/clients/magic/client.ts @@ -0,0 +1,244 @@ +/** + * Documentation: + * https://magic.link/docs/blockchains/other-chains/other/algorand + */ +import Algod, { getAlgodClient } from '../../algod' +import BaseClient from '../base' +import { DEFAULT_NETWORK, PROVIDER_ID } from '../../constants' +import { debugLog } from '../../utils/debugLog' +import { ICON } from './constants' +import type { DecodedSignedTransaction, DecodedTransaction, Network } from '../../types/node' +import type { InitParams } from '../../types/providers' +import type { Wallet } from '../../types/wallet' +import type { MagicAuthTransaction, MagicAuthConstructor, MagicAuthConnectOptions } from './types' +import { AlgorandExtension } from '@magic-ext/algorand' +import { SDKBase, InstanceWithExtensions } from '@magic-sdk/provider' + +class MagicAuth extends BaseClient { + #client: InstanceWithExtensions< + SDKBase, + { + algorand: AlgorandExtension + } + > + clientOptions?: MagicAuthConnectOptions + network: Network + + constructor({ + metadata, + client, + clientOptions, + algosdk, + algodClient, + network + }: MagicAuthConstructor) { + super(metadata, algosdk, algodClient) + this.#client = client + this.clientOptions = clientOptions + this.network = network + this.metadata = MagicAuth.metadata + } + + static metadata = { + id: PROVIDER_ID.MAGIC, + name: 'Magic', + icon: ICON, + isWalletConnect: true + } + + static async init({ + clientOptions, + algodOptions, + clientStatic, + getDynamicClient, + algosdkStatic, + network = DEFAULT_NETWORK + }: InitParams): Promise { + try { + debugLog(`${PROVIDER_ID.MAGIC.toUpperCase()} initializing...`) + + let Magic + if (clientStatic) { + Magic = clientStatic + } else if (getDynamicClient) { + Magic = await getDynamicClient() + } else if (!clientOptions || !clientOptions.apiKey) { + throw new Error( + 'Magic provider missing API Key to be passed by required property: clientOptions' + ) + } else { + throw new Error( + 'Magic provider missing required property: clientStatic or getDynamicClient' + ) + } + + const algosdk = algosdkStatic || (await Algod.init(algodOptions)).algosdk + const algodClient = getAlgodClient(algosdk, algodOptions) + + const magic = new Magic(clientOptions?.apiKey as string, { + extensions: { + algorand: new AlgorandExtension({ + rpcUrl: '' + }) + } + }) + + const provider = new MagicAuth({ + metadata: MagicAuth.metadata, + client: magic, + clientOptions: clientOptions as MagicAuthConnectOptions, + algosdk, + algodClient, + network + }) + + debugLog(`${PROVIDER_ID.MAGIC.toUpperCase()} initialized`, '✅') + + return provider + } catch (e) { + console.error('Error initializing...', e) + return null + } + } + + async connect(_: () => void, email: string): Promise { + await this.#client.auth.loginWithMagicLink({ email: email }) + + const userInfo = await this.#client.user.getMetadata() + + return { + ...MagicAuth.metadata, + accounts: [ + { + name: `MagicWallet ${userInfo.email ?? ''} 1`, + address: userInfo.publicAddress ?? 'N/A', + providerId: MagicAuth.metadata.id + } + ] + } + } + + async reconnect() { + const isLoggedIn = await this.#client.user.isLoggedIn() + + if (!isLoggedIn) { + return null + } + + const userInfo = await this.#client.user.getMetadata() + + return { + ...MagicAuth.metadata, + accounts: [ + { + name: `MagicWallet ${userInfo.email ?? ''} 1`, + address: userInfo.publicAddress ?? 'N/A', + providerId: MagicAuth.metadata.id + } + ] + } + } + + async disconnect() { + await this.#client.user.logout() + } + + async signTransactions( + connectedAccounts: string[], + txnGroups: Uint8Array[] | Uint8Array[][], + indexesToSign?: number[], + returnGroup = true + ) { + // If txnGroups is a nested array, flatten it + const transactions: Uint8Array[] = Array.isArray(txnGroups[0]) + ? (txnGroups as Uint8Array[][]).flatMap((txn) => txn) + : (txnGroups as Uint8Array[]) + + // Decode the transactions to access their properties. + const decodedTxns = transactions.map((txn) => { + return this.algosdk.decodeObj(txn) + }) as Array + + const signedIndexes: number[] = [] + + // Marshal the transactions, + // and add the signers property if they shouldn't be signed. + const txnsToSign = decodedTxns.reduce((acc, txn, i) => { + const isSigned = 'txn' in txn + + // If the indexes to be signed is specified, designate that it should be signed + if (indexesToSign && indexesToSign.length && indexesToSign.includes(i)) { + signedIndexes.push(i) + acc.push({ + txn: Buffer.from(transactions[i]).toString('base64') + }) + // If the indexes to be signed is specified, but it's not included in it, + // designate that it should not be signed + } else if (indexesToSign && indexesToSign.length && !indexesToSign.includes(i)) { + acc.push({ + txn: isSigned + ? Buffer.from( + this.algosdk.encodeUnsignedTransaction( + this.algosdk.decodeSignedTransaction(transactions[i]).txn + ) + ).toString('base64') + : Buffer.from(transactions[i]).toString('base64'), + signers: [] + }) + // If the transaction is unsigned and is to be sent from a connected account, + // designate that it should be signed + } else if (!isSigned && connectedAccounts.includes(this.algosdk.encodeAddress(txn['snd']))) { + signedIndexes.push(i) + acc.push({ + txn: Buffer.from(transactions[i]).toString('base64') + }) + // Otherwise, designate that it should not be signed + } else if (isSigned) { + acc.push({ + txn: Buffer.from( + this.algosdk.encodeUnsignedTransaction( + this.algosdk.decodeSignedTransaction(transactions[i]).txn + ) + ).toString('base64'), + signers: [] + }) + } else if (!isSigned) { + acc.push({ + txn: Buffer.from(transactions[i]).toString('base64'), + signers: [] + }) + } + + return acc + }, []) + + // Sign them with the client. + try { + console.log(txnsToSign) + const result = await this.#client.algorand.signGroupTransactionV2(txnsToSign) + console.log(result) + const decodedSignedTxns: Uint8Array[] = result.map((txn: string) => { + return Buffer.from(txn, 'base64') + }) + + // Join the newly signed transactions with the original group of transactions. + const signedTxns = transactions.reduce((acc, txn, i) => { + if (signedIndexes.includes(i)) { + const signedByUser = decodedSignedTxns.shift() + signedByUser && acc.push(signedByUser) + } else if (returnGroup) { + acc.push(transactions[i]) + } + + return acc + }, []) + + return signedTxns + } catch (e) { + console.error(e) + return [] + } + } +} + +export default MagicAuth diff --git a/src/clients/magic/constants.ts b/src/clients/magic/constants.ts new file mode 100644 index 00000000..b3a124cb --- /dev/null +++ b/src/clients/magic/constants.ts @@ -0,0 +1,3 @@ +export const ICON = + 'data:image/svg+xml;base64,' + + 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLSBHZW5lcmF0ZWQgYnkgUGl4ZWxtYXRvciBQcm8gMy40LjMgLS0+Cjxzdmcgd2lkdGg9IjQ3IiBoZWlnaHQ9IjQ3IiB2aWV3Qm94PSIwIDAgNDcgNDciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBpZD0iUGF0aCIgZmlsbD0iIzY4NTFmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9Im5vbmUiIGQ9Ik0gMjMuOTYwODYxIDEuODA3NjkgQyAyNS44MzUwNzcgNC4xMDMxNTMgMjcuOTAyMjE2IDYuMjM0ODkgMzAuMTM3NTM5IDguMTc4MTY5IEMgMjguNjQ3OTY4IDEzLjAwOTMyMyAyNy44NDYwOTIgMTguMTQyMDk0IDI3Ljg0NjA5MiAyMy40NjIxNTQgQyAyNy44NDYwOTIgMjguNzgyMzA3IDI4LjY0ODA2MiAzMy45MTUxNjkgMzAuMTM3NjMgMzguNzQ2MzY4IEMgMjcuOTAyMjE2IDQwLjY4OTcyNCAyNS44MzUwNzcgNDIuODIxNDc2IDIzLjk2MDg2MSA0NS4xMTY5ODUgQyAyMi4wODY1NTQgNDIuODIxNDc2IDIwLjAxOTQxNSA0MC42ODk2MzIgMTcuNzgzOTk4IDM4Ljc0NjM2OCBDIDE5LjI3MzQ3NiAzMy45MTUxNjkgMjAuMDc1NDQ1IDI4Ljc4MjQgMjAuMDc1NDQ1IDIzLjQ2MjMzNyBDIDIwLjA3NTQ0NSAxOC4xNDIyNzcgMTkuMjczNDc2IDEzLjAwOTUwNiAxNy43ODM5OTggOC4xNzgzMTggQyAyMC4wMTk0MTUgNi4yMzUwMDEgMjIuMDg2NTU0IDQuMTAzMjEgMjMuOTYwODYxIDEuODA3NjkgWiBNIDEzLjUxMTQyNyAzNS40MDY0MDMgQyAxMS4xNDUxMzkgMzMuNzQ3ODE0IDguNjMzODE2IDMyLjI4MjA2MyA2LjAwMDI2OSAzMS4wMzE5MzcgQyA2LjczMDc3NiAyOC42Mzc0NzYgNy4xMjM3NTQgMjYuMDk1NzgzIDcuMTIzNzU0IDIzLjQ2MjQyOSBDIDcuMTIzNzU0IDIwLjgyODg5MiA2LjczMDc2MiAxOC4yODcyMDEgNi4wMDAyMzUgMTUuODkyNzM4IEMgOC42MzM4MTYgMTQuNjQyNjE2IDExLjE0NTE3NSAxMy4xNzY4NjEgMTMuNTExNTAxIDExLjUxODI3NiBDIDE0LjQxNjMxMSAxNS4zNTI1NTQgMTQuODk1MDc0IDE5LjM1MTQxNCAxNC44OTUwNzQgMjMuNDYyMTU0IEMgMTQuODk1MDc0IDI3LjU3Mjk4NSAxNC40MTYyODMgMzEuNTcxOTM4IDEzLjUxMTQyNyAzNS40MDY0MDMgWiBNIDMzLjAyNzA0NiAyMy40NjIzMzcgQyAzMy4wMjcwNDYgMjcuNTcyOTg1IDMzLjUwNTc1MyAzMS41NzE4NDYgMzQuNDEwNTUzIDM1LjQwNjEyNCBDIDM2Ljc3Njg1OSAzMy43NDc2MzEgMzkuMjg4MDk0IDMyLjI4MTg3NiA0MS45MjE1MzkgMzEuMDMxODQ1IEMgNDEuMTkxMDE3IDI4LjYzNzM4NCA0MC43OTgwNjEgMjYuMDk1NjkyIDQwLjc5ODA2MSAyMy40NjIyNDYgQyA0MC43OTgwNjEgMjAuODI4OCA0MS4xOTEwMTcgMTguMjg3MjAxIDQxLjkyMTUzOSAxNS44OTI4MyBDIDM5LjI4ODA5NCAxNC42NDI3MDggMzYuNzc2NzY4IDEzLjE3NzA0OCAzNC40MTA1NTMgMTEuNTE4NTU1IEMgMzMuNTA1NzUzIDE1LjM1MjgzMSAzMy4wMjcwNDYgMTkuMzUxNjkyIDMzLjAyNzA0NiAyMy40NjIzMzcgWiIvPgo8L3N2Zz4K' diff --git a/src/clients/magic/index.ts b/src/clients/magic/index.ts new file mode 100644 index 00000000..fea7fc3b --- /dev/null +++ b/src/clients/magic/index.ts @@ -0,0 +1,3 @@ +import pera from './client' + +export default pera diff --git a/src/clients/magic/types.ts b/src/clients/magic/types.ts new file mode 100644 index 00000000..2f991bd2 --- /dev/null +++ b/src/clients/magic/types.ts @@ -0,0 +1,33 @@ +import type algosdk from 'algosdk' +import type { Network } from '../../types/node' +import type { Metadata } from '../../types/wallet' +import { SDKBase, InstanceWithExtensions } from '@magic-sdk/provider' +import { AlgorandExtension } from '@magic-ext/algorand' + +export type MagicAuthConnectOptions = { + apiKey: string +} + +export interface MagicAuthTransaction { + txn: string + /** + * Optional list of addresses that must sign the transactions. + * Wallet skips to sign this txn if signers is empty array. + * If undefined, wallet tries to sign it. + */ + signers?: string[] +} + +export type MagicAuthConstructor = { + metadata: Metadata + client: InstanceWithExtensions< + SDKBase, + { + algorand: AlgorandExtension + } + > + clientOptions: MagicAuthConnectOptions + algosdk: typeof algosdk + algodClient: algosdk.Algodv2 + network: Network +} diff --git a/src/components/Example/Connect.tsx b/src/components/Example/Connect.tsx index 2ed85797..f691dfe3 100644 --- a/src/components/Example/Connect.tsx +++ b/src/components/Example/Connect.tsx @@ -1,8 +1,9 @@ -import React from 'react' -import { useWallet } from '../../index' +import React, { useState } from 'react' +import { PROVIDER_ID, useWallet } from '../../index' export default function ConnectWallet() { const { providers, connectedAccounts, connectedActiveAccounts, activeAccount } = useWallet() + const [email, setEmail] = useState('') // Use these properties to display connected accounts to users. React.useEffect(() => { @@ -24,7 +25,23 @@ export default function ConnectWallet() { {provider.metadata.name} {provider.isActive && '[active]'}{' '}
-