diff --git a/README.md b/README.md index a863c7c3..6f731843 100644 --- a/README.md +++ b/README.md @@ -513,6 +513,11 @@ useEffect(() => { - Download - https://kibis.is/#download - Support/Issues - https://discord.com/channels/1055863853633785857/1181252381816655952 +### Magic Auth + +- Website - https://magic.link/ +- Install dependency - `npm install magic-sdk @magic-ext/algorand` + ### KMD (Algorand Key Management Daemon) - Documentation - https://developer.algorand.org/docs/rest-apis/kmd @@ -655,6 +660,7 @@ import { PeraWalletConnect } from '@perawallet/connect' import { DaffiWalletConnect } from '@daffiwallet/connect' import { WalletConnectModalSign } from '@walletconnect/modal-sign-html' import LuteConnect from 'lute-connect' +import { Magic } from 'magic-sdk'; export default function App() { const providers = useInitializeProviders({ @@ -685,6 +691,12 @@ export default function App() { clientOptions: { siteName: 'YourSiteName' } }, { id: PROVIDER_ID.KIBISIS } + }, + { + id: PROVIDER_ID.MAGIC, + getDynamicClient: Magic, + clientOptions: { apiKey: 'API_KEY' }, + }, ], nodeConfig: { network: 'mainnet', diff --git a/babel.config.js b/babel.config.js index 6003ed75..9f5ee387 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,4 @@ module.exports = { - presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'] + presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], + plugins: [['@babel/plugin-transform-class-properties', { loose: true }]] } diff --git a/package.json b/package.json index 149f72d7..762880c7 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,13 @@ }, "devDependencies": { "@babel/core": "^7.16.0", + "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/preset-env": "^7.16.4", "@babel/preset-react": "^7.16.0", "@babel/preset-typescript": "^7.16.0", "@blockshake/defly-connect": "^1.1.5", "@daffiwallet/connect": "^1.0.3", + "@magic-ext/algorand": "^17.0.0", "@perawallet/connect": "^1.2.1", "@randlabs/myalgo-connect": "^1.4.2", "@release-it/conventional-changelog": "^8.0.0", @@ -80,6 +82,7 @@ "jest-canvas-mock": "^2.5.0", "jest-environment-jsdom": "^29.3.1", "lute-connect": "^1.0.7", + "magic-sdk": "^22.0.0", "postcss": "^8.4.17", "prettier": "^3.2.4", "react": "^18.2.0", @@ -103,13 +106,21 @@ "peerDependencies": { "@blockshake/defly-connect": "^1.1.5", "@daffiwallet/connect": "^1.0.3", + "@magic-ext/algorand": "^17.0.0", "@perawallet/connect": "^1.2.1", "@randlabs/myalgo-connect": "^1.4.2", "algosdk": "^2.1.0", + "magic-sdk": "^22.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "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 16fefbb8..4fc8ba75 100644 --- a/src/clients/base/base.ts +++ b/src/clients/base/base.ts @@ -24,7 +24,8 @@ abstract class BaseClient { static metadata: Metadata - abstract connect(onDisconnect: () => void): Promise + // eslint-disable-next-line @typescript-eslint/no-explicit-any + abstract connect(onDisconnect: () => void, arg?: any): Promise abstract disconnect(): Promise abstract reconnect(onDisconnect: () => void): Promise abstract signTransactions( diff --git a/src/clients/index.ts b/src/clients/index.ts index af7ac450..9896fc49 100644 --- a/src/clients/index.ts +++ b/src/clients/index.ts @@ -6,6 +6,7 @@ import exodus from './exodus' import algosigner from './algosigner' import lute from './lute' import walletconnect from './walletconnect2' +import magic from './magic' import kmd from './kmd' import mnemonic from './mnemonic' import { CustomProvider } from './custom/types' @@ -24,7 +25,8 @@ export { mnemonic, custom, CustomProvider, - kibisis + kibisis, + magic } export default { @@ -39,5 +41,6 @@ export default { [kmd.metadata.id]: kmd, [mnemonic.metadata.id]: mnemonic, [custom.metadata.id]: custom, - [kibisis.metadata.id]: kibisis + [kibisis.metadata.id]: kibisis, + [magic.metadata.id]: magic } diff --git a/src/clients/magic/client.ts b/src/clients/magic/client.ts new file mode 100644 index 00000000..3c985134 --- /dev/null +++ b/src/clients/magic/client.ts @@ -0,0 +1,251 @@ +/** + * 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 + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async connect(_: () => void, arg?: any): Promise { + if (!arg || typeof arg !== 'string') { + throw new Error('Magic Link provider requires an email (string) to connect') + } + + const email = arg + await this.#client.auth.loginWithMagicLink({ email: email }) + const userInfo = await this.#client.user.getInfo() + + return { + ...MagicAuth.metadata, + accounts: [ + { + name: `MagicWallet ${userInfo.email ?? ''} 1`, + address: userInfo.publicAddress ?? 'N/A', + providerId: MagicAuth.metadata.id, + email: userInfo.email ?? '' + } + ] + } + } + + async reconnect() { + const isLoggedIn = await this.#client.user.isLoggedIn() + + if (!isLoggedIn) { + return null + } + + const userInfo = await this.#client.user.getInfo() + + return { + ...MagicAuth.metadata, + accounts: [ + { + name: `MagicWallet ${userInfo.email ?? ''} 1`, + address: userInfo.publicAddress ?? 'N/A', + providerId: MagicAuth.metadata.id, + email: userInfo.email ?? '' + } + ] + } + } + + 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..70f0ce72 --- /dev/null +++ b/src/clients/magic/index.ts @@ -0,0 +1,3 @@ +import magic from './client' + +export default magic 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..48317165 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,20 @@ export default function ConnectWallet() { {provider.metadata.name} {provider.isActive && '[active]'}{' '}
-