@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
154 lines (139 loc) • 6.33 kB
text/typescript
import { CWIStyleWalletManager, UMPTokenInteractor, OverlayUMPTokenInteractor } from './CWIStyleWalletManager'
import { PrivilegedKeyManager } from './sdk/PrivilegedKeyManager'
import { WalletInterface, Random, Utils, Transaction, RPuzzle, PrivateKey, BigNumber } from '@bsv/sdk'
import { WABClient } from './wab-client/WABClient'
import { AuthMethodInteractor } from './wab-client/auth-method-interactors/AuthMethodInteractor'
/**
* WalletAuthenticationManager
*
* A wallet manager that integrates
* with a WABClient for user authentication flows (e.g. Twilio phone).
*/
export class WalletAuthenticationManager extends CWIStyleWalletManager {
private wabClient: WABClient // instance of WABClient
private authMethod?: AuthMethodInteractor // chosen AuthMethod interactor
private tempPresentationKey?: string // for temporary persistence between auth steps
constructor(
adminOriginator: string,
walletBuilder: (primaryKey: number[], privilegedKeyManager: PrivilegedKeyManager) => Promise<WalletInterface>,
interactor: UMPTokenInteractor = new OverlayUMPTokenInteractor(),
recoveryKeySaver: (key: number[]) => Promise<true>,
passwordRetriever: (reason: string, test: (passwordCandidate: string) => boolean) => Promise<string>,
wabClient: WABClient,
authMethod?: AuthMethodInteractor,
stateSnapshot?: number[]
) {
super(
adminOriginator,
walletBuilder,
interactor,
recoveryKeySaver,
passwordRetriever,
// Here, we provide a custom new wallet funder that uses the Secret Server
async (presentationKey: number[], wallet: WalletInterface, adminOriginator: string) => {
debugger
const { paymentData } = await this.wabClient.requestFaucet(Utils.toHex(presentationKey))
if (!paymentData.k || !paymentData.tx) {
throw new Error('Invalid')
}
const tx = Transaction.fromAtomicBEEF(paymentData.tx as number[])
console.log(paymentData)
const faucetRedeemTXCreationResult = await wallet.createAction(
{
inputBEEF: tx.toBEEF(),
inputs: [
{
outpoint: `${paymentData.txid}.0`,
unlockingScriptLength: 108,
inputDescription: 'Fund from faucet'
}
],
description: 'Fund wallet',
options: {
acceptDelayedBroadcast: false
}
},
adminOriginator
)
const faucetRedeemTX = Transaction.fromAtomicBEEF(faucetRedeemTXCreationResult.signableTransaction!.tx)
const faucetRedemptionPuzzle = new RPuzzle()
const randomRedemptionPrivateKey = PrivateKey.fromRandom()
const faucetRedeemUnlocker = faucetRedemptionPuzzle.unlock(
new BigNumber(paymentData.k, 16),
randomRedemptionPrivateKey
)
const faucetRedeemUnlockingScript = await faucetRedeemUnlocker.sign(faucetRedeemTX, 0)
const signActionResult = await wallet.signAction({
reference: faucetRedeemTXCreationResult.signableTransaction!.reference,
spends: {
0: {
unlockingScript: faucetRedeemUnlockingScript.toHex()
}
}
})
console.log('Sign action result:', signActionResult)
},
stateSnapshot
)
this.wabClient = wabClient
this.authMethod = authMethod
}
/**
* Sets (or switches) the chosen AuthMethodInteractor at runtime,
* in case the user changes their mind or picks a new method in the UI.
*/
public setAuthMethod(method: AuthMethodInteractor) {
this.authMethod = method
}
/**
* Initiate the WAB-based flow, e.g. sending an SMS code or starting an ID check,
* using the chosen AuthMethodInteractor.
*/
public async startAuth(payload: any): Promise<void> {
if (!this.authMethod) {
throw new Error('No AuthMethod selected in WalletAuthenticationManager')
}
this.tempPresentationKey = this.generateTemporaryPresentationKey()
// For example, if this.authMethod is Twilio, `payload` might contain { phoneNumber: "+1..." }
const startRes = await this.wabClient.startAuthMethod(
this.authMethod,
// The user might not have a presentationKey yet, so we generate one locally to pass to the server.
// If it’s an existing user on the WAB side, the WAB will give us the stored key later.
// But we do need some placeholder key for the 'startAuth' call:
this.tempPresentationKey,
payload
)
if (!startRes.success) {
throw new Error(startRes.message || 'Failed to start WAB auth method')
}
}
/**
* Completes the WAB-based flow, retrieving the final presentationKey from WAB if successful.
*/
public async completeAuth(payload: any): Promise<void> {
if (!this.authMethod || !this.tempPresentationKey) {
throw new Error('No AuthMethod selected in WalletAuthenticationManager or startAuth has yet to be called.')
}
// Unser the temp presentation key early (for security)
const tempKey = this.tempPresentationKey
this.tempPresentationKey = undefined
const result = await this.wabClient.completeAuthMethod(this.authMethod, tempKey, payload)
if (!result.success || !result.presentationKey) {
throw new Error(result.message || 'Failed to complete WAB auth')
}
// We now have the final 256-bit key in hex from the WAB
const presentationKeyHex = result.presentationKey
const presentationKeyBytes = Utils.toArray(presentationKeyHex, 'hex')
// Provide this presentation key to the underlying CWI logic:
// This sets 'this.presentationKey' and determines if it's a new or existing user
// based on whether a UMP token exists on chain. We'll do "presentation-key-and-password"
// for new user flows, for example, or user might choose "presentation-key-and-recovery-key" if we wanted.
// Either way, WAB has now done its job and we are off to base-layer CWI logic at this point!
await this.providePresentationKey(presentationKeyBytes)
}
private generateTemporaryPresentationKey(): string {
// For the 'startAuth' call, we can generate a random 32 bytes → 64 hex chars.
const randomBytes = Random(32) // array of length 32
return Utils.toHex(randomBytes)
}
}