Skip to content

Commit

Permalink
feat: introducing magic.link provider (email based authentication) (#124
Browse files Browse the repository at this point in the history
)

* feat: introducing magic auth provider

* chore: adding email into account object as optional

* chore: bumping magic sdk; fixing merge conflicts

* docs: updating docs

* refactor: addressing pr comments

* chore: patching babel class prop compatibility

* chore: minor tweaks

* chore: prettier tweaks

* chore: addressing pr changes

* chore: minor typo
  • Loading branch information
aorumbayev authored Jan 30, 2024
1 parent 380bd15 commit 85f8bef
Show file tree
Hide file tree
Showing 19 changed files with 3,822 additions and 3,097 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -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 }]]
}
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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
},
Expand Down
3 changes: 2 additions & 1 deletion src/clients/base/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ abstract class BaseClient {

static metadata: Metadata

abstract connect(onDisconnect: () => void): Promise<Wallet>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
abstract connect(onDisconnect: () => void, arg?: any): Promise<Wallet>
abstract disconnect(): Promise<void>
abstract reconnect(onDisconnect: () => void): Promise<Wallet | null>
abstract signTransactions(
Expand Down
7 changes: 5 additions & 2 deletions src/clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,7 +25,8 @@ export {
mnemonic,
custom,
CustomProvider,
kibisis
kibisis,
magic
}

export default {
Expand All @@ -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
}
251 changes: 251 additions & 0 deletions src/clients/magic/client.ts
Original file line number Diff line number Diff line change
@@ -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<PROVIDER_ID.MAGIC>): Promise<BaseClient | null> {
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<Wallet> {
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<DecodedTransaction | DecodedSignedTransaction>

const signedIndexes: number[] = []

// Marshal the transactions,
// and add the signers property if they shouldn't be signed.
const txnsToSign = decodedTxns.reduce<MagicAuthTransaction[]>((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<Uint8Array[]>((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
3 changes: 3 additions & 0 deletions src/clients/magic/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const ICON =
'data:image/svg+xml;base64,' +
'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCEtLSBHZW5lcmF0ZWQgYnkgUGl4ZWxtYXRvciBQcm8gMy40LjMgLS0+Cjxzdmcgd2lkdGg9IjQ3IiBoZWlnaHQ9IjQ3IiB2aWV3Qm94PSIwIDAgNDcgNDciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgICA8cGF0aCBpZD0iUGF0aCIgZmlsbD0iIzY4NTFmZiIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2U9Im5vbmUiIGQ9Ik0gMjMuOTYwODYxIDEuODA3NjkgQyAyNS44MzUwNzcgNC4xMDMxNTMgMjcuOTAyMjE2IDYuMjM0ODkgMzAuMTM3NTM5IDguMTc4MTY5IEMgMjguNjQ3OTY4IDEzLjAwOTMyMyAyNy44NDYwOTIgMTguMTQyMDk0IDI3Ljg0NjA5MiAyMy40NjIxNTQgQyAyNy44NDYwOTIgMjguNzgyMzA3IDI4LjY0ODA2MiAzMy45MTUxNjkgMzAuMTM3NjMgMzguNzQ2MzY4IEMgMjcuOTAyMjE2IDQwLjY4OTcyNCAyNS44MzUwNzcgNDIuODIxNDc2IDIzLjk2MDg2MSA0NS4xMTY5ODUgQyAyMi4wODY1NTQgNDIuODIxNDc2IDIwLjAxOTQxNSA0MC42ODk2MzIgMTcuNzgzOTk4IDM4Ljc0NjM2OCBDIDE5LjI3MzQ3NiAzMy45MTUxNjkgMjAuMDc1NDQ1IDI4Ljc4MjQgMjAuMDc1NDQ1IDIzLjQ2MjMzNyBDIDIwLjA3NTQ0NSAxOC4xNDIyNzcgMTkuMjczNDc2IDEzLjAwOTUwNiAxNy43ODM5OTggOC4xNzgzMTggQyAyMC4wMTk0MTUgNi4yMzUwMDEgMjIuMDg2NTU0IDQuMTAzMjEgMjMuOTYwODYxIDEuODA3NjkgWiBNIDEzLjUxMTQyNyAzNS40MDY0MDMgQyAxMS4xNDUxMzkgMzMuNzQ3ODE0IDguNjMzODE2IDMyLjI4MjA2MyA2LjAwMDI2OSAzMS4wMzE5MzcgQyA2LjczMDc3NiAyOC42Mzc0NzYgNy4xMjM3NTQgMjYuMDk1NzgzIDcuMTIzNzU0IDIzLjQ2MjQyOSBDIDcuMTIzNzU0IDIwLjgyODg5MiA2LjczMDc2MiAxOC4yODcyMDEgNi4wMDAyMzUgMTUuODkyNzM4IEMgOC42MzM4MTYgMTQuNjQyNjE2IDExLjE0NTE3NSAxMy4xNzY4NjEgMTMuNTExNTAxIDExLjUxODI3NiBDIDE0LjQxNjMxMSAxNS4zNTI1NTQgMTQuODk1MDc0IDE5LjM1MTQxNCAxNC44OTUwNzQgMjMuNDYyMTU0IEMgMTQuODk1MDc0IDI3LjU3Mjk4NSAxNC40MTYyODMgMzEuNTcxOTM4IDEzLjUxMTQyNyAzNS40MDY0MDMgWiBNIDMzLjAyNzA0NiAyMy40NjIzMzcgQyAzMy4wMjcwNDYgMjcuNTcyOTg1IDMzLjUwNTc1MyAzMS41NzE4NDYgMzQuNDEwNTUzIDM1LjQwNjEyNCBDIDM2Ljc3Njg1OSAzMy43NDc2MzEgMzkuMjg4MDk0IDMyLjI4MTg3NiA0MS45MjE1MzkgMzEuMDMxODQ1IEMgNDEuMTkxMDE3IDI4LjYzNzM4NCA0MC43OTgwNjEgMjYuMDk1NjkyIDQwLjc5ODA2MSAyMy40NjIyNDYgQyA0MC43OTgwNjEgMjAuODI4OCA0MS4xOTEwMTcgMTguMjg3MjAxIDQxLjkyMTUzOSAxNS44OTI4MyBDIDM5LjI4ODA5NCAxNC42NDI3MDggMzYuNzc2NzY4IDEzLjE3NzA0OCAzNC40MTA1NTMgMTEuNTE4NTU1IEMgMzMuNTA1NzUzIDE1LjM1MjgzMSAzMy4wMjcwNDYgMTkuMzUxNjkyIDMzLjAyNzA0NiAyMy40NjIzMzcgWiIvPgo8L3N2Zz4K'
3 changes: 3 additions & 0 deletions src/clients/magic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import magic from './client'

export default magic
Loading

0 comments on commit 85f8bef

Please sign in to comment.