Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introducing magic.link provider (email based authentication) #124

Merged
merged 13 commits into from
Jan 30, 2024
Merged
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)
aorumbayev marked this conversation as resolved.
Show resolved Hide resolved
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