Skip to content

Commit

Permalink
feat: introducing magic auth provider
Browse files Browse the repository at this point in the history
  • Loading branch information
aorumbayev committed Dec 11, 2023
1 parent f825a1b commit 5652252
Show file tree
Hide file tree
Showing 16 changed files with 1,145 additions and 25 deletions.
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
},
Expand Down
2 changes: 1 addition & 1 deletion src/clients/base/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ abstract class BaseClient {

static metadata: Metadata

abstract connect(onDisconnect: () => void): Promise<Wallet>
abstract connect(onDisconnect: () => void, email?: string): Promise<Wallet>
abstract disconnect(): Promise<void>
abstract reconnect(onDisconnect: () => void): Promise<Wallet | null>
abstract signTransactions(
Expand Down
3 changes: 3 additions & 0 deletions src/clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -20,6 +21,7 @@ export {
kmd,
mnemonic,
custom,
magic,
CustomProvider
}

Expand All @@ -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
}
244 changes: 244 additions & 0 deletions src/clients/magic/client.ts
Original file line number Diff line number Diff line change
@@ -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<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
}
}

async connect(_: () => void, email: string): Promise<Wallet> {
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<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 pera from './client'

export default pera
33 changes: 33 additions & 0 deletions src/clients/magic/types.ts
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 20 additions & 3 deletions src/components/Example/Connect.tsx
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -24,7 +25,23 @@ export default function ConnectWallet() {
{provider.metadata.name} {provider.isActive && '[active]'}{' '}
</h4>
<div>
<button onClick={provider.connect} disabled={provider.isConnected}>
{provider.metadata.id === PROVIDER_ID.MAGIC && (
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
)}
<button
onClick={() => {
provider.connect(
undefined,
provider.metadata.id === PROVIDER_ID.MAGIC ? email : undefined
)
}}
disabled={provider.isConnected}
>
Connect
</button>
<button onClick={provider.disconnect} disabled={!provider.isConnected}>
Expand Down
Loading

0 comments on commit 5652252

Please sign in to comment.