Created
July 6, 2025 03:03
-
-
Save drichar/940298f2b31a4f01df90434eeb6052e9 to your computer and use it in GitHub Desktop.
TypeScript utility module that integrates Web3Auth Single Factor Auth with Algorand blockchain for React Native applications, enabling Firebase JWT-based authentication and Algorand transaction signing through Web3Auth's managed private keys
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CHAIN_NAMESPACES, IBaseProvider } from '@web3auth/base' | |
import { CommonPrivateKeyProvider } from '@web3auth/base-provider' | |
import { Web3Auth, SDK_MODE } from '@web3auth/single-factor-auth' | |
import algosdk from 'algosdk' | |
// Create an in-memory storage that doesn't persist between app sessions | |
// This prevents automatic session restoration while satisfying Web3Auth's storage requirement | |
class InMemoryStorage { | |
private storage: Map<string, string> = new Map() | |
async getItem(key: string): Promise<string | null> { | |
return this.storage.get(key) || null | |
} | |
async setItem(key: string, value: string): Promise<void> { | |
this.storage.set(key, value) | |
} | |
async removeItem(key: string): Promise<void> { | |
this.storage.delete(key) | |
} | |
async clear(): Promise<void> { | |
this.storage.clear() | |
} | |
} | |
// Configure the chain config for Algorand | |
const chainConfig = { | |
chainNamespace: CHAIN_NAMESPACES.OTHER, | |
chainId: 'algorand', | |
rpcTarget: 'https://mainnet-api.algonode.cloud', | |
displayName: 'Algorand Mainnet', | |
blockExplorer: 'https://allo.info', | |
ticker: 'ALGO', | |
tickerName: 'Algorand', | |
} | |
// Create the private key provider for Algorand | |
const privateKeyProvider = new CommonPrivateKeyProvider({ | |
config: { chainConfig }, | |
}) as IBaseProvider<string> | |
// Configure Web3Auth Single Factor Auth instance | |
export const web3auth = new Web3Auth({ | |
clientId: process.env.EXPO_PUBLIC_WEB3AUTH_CLIENT_ID ?? '', | |
web3AuthNetwork: 'sapphire_mainnet', // or 'sapphire_devnet' for development | |
// Use in-memory storage to prevent session persistence between app restarts | |
storage: new InMemoryStorage(), | |
mode: SDK_MODE.REACT_NATIVE, | |
privateKeyProvider, | |
}) | |
/** | |
* Initializes Web3Auth SFA | |
*/ | |
export const initWeb3Auth = async (): Promise<void> => { | |
try { | |
await web3auth.init() | |
console.log('Web3Auth SFA initialized successfully') | |
} catch (error) { | |
console.error('Failed to initialize Web3Auth SFA:', error) | |
throw error | |
} | |
} | |
/** | |
* Connects to Web3Auth using Firebase JWT | |
* @param idToken The Firebase ID token | |
* @param verifierId The user's Firebase UID (sub claim) | |
* @returns Promise that resolves when connected | |
*/ | |
export const connectWeb3Auth = async ( | |
idToken: string, | |
verifierId: string, | |
): Promise<void> => { | |
const verifier = 'web3auth-haystack' // Verifier name from Web3Auth dashboard | |
await web3auth.connect({ | |
verifier, | |
verifierId, | |
idToken, | |
}) | |
} | |
/** | |
* Gets the current Web3Auth private key | |
* @returns The private key as a hex string | |
*/ | |
export const getWeb3AuthPrivateKey = async (): Promise<string> => { | |
if (!web3auth.connected) { | |
throw new Error('Web3Auth not connected') | |
} | |
// Get the private key using the provider request method | |
const privateKey = (await web3auth.provider?.request({ | |
method: 'private_key', | |
})) as string | |
if (!privateKey) { | |
throw new Error('No private key available from Web3Auth') | |
} | |
return privateKey | |
} | |
/** | |
* Creates a transaction signer function that uses Web3Auth to sign Algorand transactions | |
* @returns A transaction signer function compatible with the NFD SDK and algosdk TransactionSigner type | |
*/ | |
export const createWeb3AuthSigner = (): algosdk.TransactionSigner => { | |
/** | |
* Signs a group of transactions using Web3Auth private key | |
* @param txnGroup Array of transactions to sign | |
* @param indexesToSign Array of indexes in the transaction group that should be signed | |
* @returns A promise which resolves an array of encoded signed transactions. The length of the | |
* array will be the same as the length of indexesToSign, and each index i in the array | |
* corresponds to the signed transaction from txnGroup[indexesToSign[i]] | |
*/ | |
return async ( | |
txnGroup: algosdk.Transaction[], | |
indexesToSign: number[], | |
): Promise<Uint8Array[]> => { | |
// If there's nothing to sign, return an empty array | |
if (indexesToSign.length === 0) { | |
return [] | |
} | |
try { | |
// Get the private key from Web3Auth | |
const privateKey = await getWeb3AuthPrivateKey() | |
if (!privateKey) { | |
throw new Error('No private key available from Web3Auth') | |
} | |
// Convert hex private key to Uint8Array if needed | |
const cleanPrivateKey = privateKey.startsWith('0x') | |
? privateKey.slice(2) | |
: privateKey | |
const privateKeyBytes = new Uint8Array( | |
Buffer.from(cleanPrivateKey, 'hex'), | |
) | |
// Create Algorand account from private key | |
const account = algosdk.mnemonicToSecretKey( | |
algosdk.secretKeyToMnemonic(privateKeyBytes), | |
) | |
const signedTxns: Uint8Array[] = [] | |
// Sign each transaction that matches our indexes | |
for (const index of indexesToSign) { | |
const txn = txnGroup[index] | |
// Convert sender Address to string for comparison | |
const senderAddress = txn.sender.toString() | |
// Verify this transaction is meant to be signed by this account | |
if (senderAddress !== account.addr.toString()) { | |
throw new Error( | |
`Transaction at index ${index} has sender ${senderAddress} but expected ${account.addr}`, | |
) | |
} | |
// Sign the transaction | |
const signedTxn = algosdk.signTransaction(txn, account.sk) | |
signedTxns.push(signedTxn.blob) | |
} | |
console.log(`Signed ${signedTxns.length} transactions with Web3Auth`) | |
return signedTxns | |
} catch (error) { | |
console.error('Web3Auth transaction signing failed:', error) | |
throw new Error(`Failed to sign transactions: ${error}`) | |
} | |
} | |
} | |
/** | |
* Gets the Algorand address from the current Web3Auth private key | |
* @returns The Algorand address | |
*/ | |
export const getAlgorandAddressFromWeb3Auth = async (): Promise<string> => { | |
try { | |
const privateKey = await getWeb3AuthPrivateKey() | |
if (!privateKey) { | |
throw new Error('No private key available from Web3Auth') | |
} | |
return getAlgorandAddressFromPrivateKey(privateKey) | |
} catch (error) { | |
console.error( | |
'Failed to derive Algorand address from Web3Auth private key:', | |
error, | |
) | |
throw new Error(`Failed to derive Algorand address: ${error}`) | |
} | |
} | |
/** | |
* Gets the Algorand address from a private key | |
* @param privateKey The private key from Web3Auth (hex string) | |
* @returns The Algorand address | |
*/ | |
export const getAlgorandAddressFromPrivateKey = ( | |
privateKey: string, | |
): string => { | |
try { | |
// Convert hex private key to Uint8Array if needed | |
const cleanPrivateKey = privateKey.startsWith('0x') | |
? privateKey.slice(2) | |
: privateKey | |
const privateKeyBytes = new Uint8Array(Buffer.from(cleanPrivateKey, 'hex')) | |
// Create Algorand account from private key | |
const account = algosdk.mnemonicToSecretKey( | |
algosdk.secretKeyToMnemonic(privateKeyBytes), | |
) | |
return account.addr.toString() | |
} catch (error) { | |
console.error('Failed to derive Algorand address from private key:', error) | |
throw new Error(`Failed to derive Algorand address: ${error}`) | |
} | |
} | |
/** | |
* Gets the Algorand mnemonic from the current Web3Auth private key | |
* @returns The 25-word mnemonic phrase | |
*/ | |
export const getAlgorandMnemonicFromWeb3Auth = async (): Promise<string> => { | |
try { | |
const privateKey = await getWeb3AuthPrivateKey() | |
if (!privateKey) { | |
throw new Error('No private key available from Web3Auth') | |
} | |
// Convert hex private key to Uint8Array if needed | |
const cleanPrivateKey = privateKey.startsWith('0x') | |
? privateKey.slice(2) | |
: privateKey | |
const privateKeyBytes = new Uint8Array(Buffer.from(cleanPrivateKey, 'hex')) | |
// Convert private key to mnemonic | |
const mnemonic = algosdk.secretKeyToMnemonic(privateKeyBytes) | |
return mnemonic | |
} catch (error) { | |
console.error('Failed to derive mnemonic from Web3Auth private key:', error) | |
throw new Error(`Failed to derive mnemonic: ${error}`) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment