wallet-storage
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
487 lines (429 loc) • 19.4 kB
text/typescript
import {
AbortActionArgs,
AbortActionResult,
AcquireCertificateArgs,
AcquireCertificateResult,
AuthenticatedResult,
Beef,
BeefParty,
CreateActionArgs,
CreateActionResult,
CreateHmacArgs,
CreateHmacResult,
CreateSignatureArgs,
CreateSignatureResult,
DiscoverByAttributesArgs,
DiscoverByIdentityKeyArgs,
DiscoverCertificatesResult,
GetHeaderArgs,
GetHeaderResult,
GetHeightResult,
GetNetworkResult,
GetPublicKeyArgs,
GetPublicKeyResult,
GetVersionResult,
InternalizeActionArgs,
InternalizeActionResult,
KeyDeriver,
ListActionsArgs,
ListActionsResult,
ListCertificatesArgs,
ListCertificatesResult,
ListOutputsArgs,
ListOutputsResult,
OriginatorDomainNameStringUnder250Bytes,
ProtoWallet,
ProveCertificateArgs,
ProveCertificateResult,
PubKeyHex,
RelinquishCertificateArgs,
RelinquishCertificateResult,
RelinquishOutputArgs,
RelinquishOutputResult,
RevealCounterpartyKeyLinkageArgs,
RevealCounterpartyKeyLinkageResult,
RevealSpecificKeyLinkageArgs,
RevealSpecificKeyLinkageResult,
SignActionArgs,
SignActionResult,
Transaction as BsvTransaction,
TrustSelf,
Utils,
VerifyHmacArgs,
VerifyHmacResult,
VerifySignatureArgs,
VerifySignatureResult,
WalletDecryptArgs,
WalletDecryptResult,
WalletEncryptArgs,
WalletEncryptResult,
WalletInterface,
} from "@bsv/sdk";
import { sdk, toWalletNetwork, Monitor, WalletStorageManager, WalletSigner } from './index.client'
import { acquireDirectCertificate } from "./signer/methods/acquireDirectCertificate";
import { proveCertificate } from "./signer/methods/proveCertificate";
import { createAction } from "./signer/methods/createAction";
import { signAction } from "./signer/methods/signAction";
import { internalizeAction } from "./signer/methods/internalizeAction";
export interface WalletArgs {
chain: sdk.Chain
keyDeriver: KeyDeriver
storage: WalletStorageManager
services?: sdk.WalletServices
monitor?: Monitor
privilegedKeyManager?: sdk.PrivilegedKeyManager
}
function isWalletSigner(args: WalletArgs | WalletSigner): args is WalletSigner {
return args["isWalletSigner"]
}
export class Wallet implements WalletInterface {
chain: sdk.Chain
keyDeriver: KeyDeriver
storage: WalletStorageManager
services?: sdk.WalletServices
monitor?: Monitor
identityKey: string
beef: BeefParty
trustSelf?: TrustSelf
userParty: string
proto: ProtoWallet
privilegedKeyManager?: sdk.PrivilegedKeyManager
pendingSignActions: Record<string, PendingSignAction>
constructor(argsOrSigner: WalletArgs | WalletSigner, services?: sdk.WalletServices, monitor?: Monitor, privilegedKeyManager?: sdk.PrivilegedKeyManager) {
const args: WalletArgs = !isWalletSigner(argsOrSigner) ? argsOrSigner : {
chain: argsOrSigner.chain,
keyDeriver: argsOrSigner.keyDeriver,
storage: argsOrSigner.storage,
services,
monitor,
privilegedKeyManager
}
if (args.storage._authId.identityKey != args.keyDeriver.identityKey)
throw new sdk.WERR_INVALID_PARAMETER('storage', `authenticated as the same identityKey (${args.storage._authId.identityKey}) as the keyDeriver (${args.keyDeriver.identityKey}).`);
this.chain = args.chain
this.keyDeriver = args.keyDeriver
this.storage = args.storage
this.proto = new ProtoWallet(args.keyDeriver)
this.services = args.services
this.monitor = args.monitor
this.privilegedKeyManager = args.privilegedKeyManager
this.identityKey = this.keyDeriver.identityKey
this.pendingSignActions = {}
this.userParty = `user ${this.getClientChangeKeyPair().publicKey}`
this.beef = new BeefParty([this.userParty])
this.trustSelf = 'known'
if (this.services) {
this.storage.setServices(this.services)
}
}
async destroy(): Promise<void> {
await this.storage.destroy()
if (this.privilegedKeyManager) await this.privilegedKeyManager.destroyKey()
}
getClientChangeKeyPair(): sdk.KeyPair {
const kp: sdk.KeyPair = {
privateKey: this.keyDeriver.rootKey.toString(),
publicKey: this.keyDeriver.rootKey.toPublicKey().toString()
}
return kp
}
async getIdentityKey(): Promise<PubKeyHex> {
return (await this.getPublicKey({ identityKey: true })).publicKey
}
getPublicKey(args: GetPublicKeyArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetPublicKeyResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.getPublicKey(args)
}
return this.proto.getPublicKey(args)
}
revealCounterpartyKeyLinkage(args: RevealCounterpartyKeyLinkageArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<RevealCounterpartyKeyLinkageResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.revealCounterpartyKeyLinkage(args)
}
return this.proto.revealCounterpartyKeyLinkage(args)
}
revealSpecificKeyLinkage(args: RevealSpecificKeyLinkageArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<RevealSpecificKeyLinkageResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.revealSpecificKeyLinkage(args)
}
return this.proto.revealSpecificKeyLinkage(args)
}
encrypt(args: WalletEncryptArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<WalletEncryptResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.encrypt(args)
}
return this.proto.encrypt(args)
}
decrypt(args: WalletDecryptArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<WalletDecryptResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.decrypt(args)
}
return this.proto.decrypt(args)
}
createHmac(args: CreateHmacArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<CreateHmacResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.createHmac(args)
}
return this.proto.createHmac(args)
}
verifyHmac(args: VerifyHmacArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<VerifyHmacResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.verifyHmac(args)
}
return this.proto.verifyHmac(args)
}
createSignature(args: CreateSignatureArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<CreateSignatureResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.createSignature(args)
}
return this.proto.createSignature(args)
}
verifySignature(args: VerifySignatureArgs, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<VerifySignatureResult> {
if (args.privileged) {
if (!this.privilegedKeyManager) {
throw new Error('Privileged operations require the Wallet to be configured with a privileged key manager.')
}
return this.privilegedKeyManager.verifySignature(args)
}
return this.proto.verifySignature(args)
}
getServices(): sdk.WalletServices {
if (!this.services)
throw new sdk.WERR_INVALID_PARAMETER('services', 'valid in constructor arguments to be retreived here.')
return this.services
}
/**
* @returns the full list of txids whose validity this wallet claims to know.
*
* @param newKnownTxids Optional. Additional new txids known to be valid by the caller to be merged.
*/
getKnownTxids(newKnownTxids?: string[]): string[] {
if (newKnownTxids) {
for (const txid of newKnownTxids) this.beef.mergeTxidOnly(txid)
}
const r = this.beef.sortTxs()
const knownTxids = r.valid
return knownTxids
}
getStorageIdentity(): sdk.StorageIdentity {
const s = this.storage.getSettings()
return { storageIdentityKey: s.storageIdentityKey, storageName: s.storageName }
}
private validateAuthAndArgs<A, T extends sdk.ValidWalletSignerArgs>(args: A, validate: (args: A) => T): { vargs: T, auth: sdk.AuthId } {
const vargs = validate(args)
const auth: sdk.AuthId = { identityKey: this.identityKey }
return { vargs, auth }
}
//////////////////
// List Methods
//////////////////
async listActions(args: ListActionsArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<ListActionsResult> {
sdk.validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, sdk.validateListActionsArgs)
const r = await this.storage.listActions(vargs)
return r
}
get storageParty(): string { return `storage ${this.getStorageIdentity().storageIdentityKey}` }
async listOutputs(args: ListOutputsArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<ListOutputsResult> {
sdk.validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, sdk.validateListOutputsArgs)
vargs.knownTxids = this.getKnownTxids()
const r = await this.storage.listOutputs(vargs)
if (r.BEEF) {
this.beef.mergeBeefFromParty(this.storageParty, r.BEEF)
}
return r
}
async listCertificates(args: ListCertificatesArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<ListCertificatesResult> {
sdk.validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, sdk.validateListCertificatesArgs)
const r = await this.storage.listCertificates(vargs)
return r
}
//////////////////
// Certificates
//////////////////
async acquireCertificate(args: AcquireCertificateArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<AcquireCertificateResult> {
sdk.validateOriginator(originator)
if (args.acquisitionProtocol === 'direct') {
const { auth, vargs } = this.validateAuthAndArgs(args, sdk.validateAcquireDirectCertificateArgs)
vargs.subject = (await this.getPublicKey({ identityKey: true, privileged: args.privileged, privilegedReason: args.privilegedReason })).publicKey
try {
// Confirm that the information received adds up to a usable certificate...
await sdk.CertOps.fromCounterparty(vargs.privileged ? this.privilegedKeyManager! : this, {
certificate: { ...vargs },
keyring: vargs.keyringForSubject,
counterparty: vargs.keyringRevealer === 'certifier' ? vargs.certifier : vargs.keyringRevealer
})
} catch (eu: unknown) {
const e = sdk.WalletError.fromUnknown(eu)
throw new sdk.WERR_INVALID_PARAMETER('args', `valid encrypted and signed certificate and keyring from revealer. ${e.name}: ${e.message}`);
}
const r = await acquireDirectCertificate(this, auth, vargs)
return r
}
if (args.acquisitionProtocol === 'issuance') {
throw new sdk.WERR_NOT_IMPLEMENTED()
}
throw new sdk.WERR_INVALID_PARAMETER('acquisitionProtocol', `valid. ${args.acquisitionProtocol} is unrecognized.`)
}
async relinquishCertificate(args: RelinquishCertificateArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<RelinquishCertificateResult> {
sdk.validateOriginator(originator)
this.validateAuthAndArgs(args, sdk.validateRelinquishCertificateArgs)
const r = await this.storage.relinquishCertificate(args)
return { relinquished: true }
}
async proveCertificate(args: ProveCertificateArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<ProveCertificateResult> {
originator = sdk.validateOriginator(originator)
const { auth, vargs } = this.validateAuthAndArgs(args, sdk.validateProveCertificateArgs)
const r = await proveCertificate(this, auth, vargs)
return r
}
async discoverByIdentityKey(args: DiscoverByIdentityKeyArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<DiscoverCertificatesResult> {
sdk.validateOriginator(originator)
this.validateAuthAndArgs(args, sdk.validateDiscoverByIdentityKeyArgs)
throw new Error("Method not implemented.");
}
async discoverByAttributes(args: DiscoverByAttributesArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<DiscoverCertificatesResult> {
sdk.validateOriginator(originator)
this.validateAuthAndArgs(args, sdk.validateDiscoverByAttributesArgs)
throw new Error("Method not implemented.");
}
//////////////////
// Actions
//////////////////
async createAction(args: CreateActionArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<CreateActionResult> {
sdk.validateOriginator(originator)
if (!args.options) args.options = {}
args.options.trustSelf ||= this.trustSelf
args.options.knownTxids = this.getKnownTxids(args.options.knownTxids)
const { auth, vargs } = this.validateAuthAndArgs(args, sdk.validateCreateActionArgs)
const r = await createAction(this, auth, vargs)
if (r.signableTransaction) {
const st = r.signableTransaction
const ab = Beef.fromBinary(st.tx)
if (!ab.atomicTxid)
throw new sdk.WERR_INTERNAL('Missing atomicTxid in signableTransaction result')
if (ab.txs.length < 1 || ab.txs[ab.txs.length - 1].txid !== ab.atomicTxid)
throw new sdk.WERR_INTERNAL('atomicTxid does not match txid of last AtomicBEEF transaction')
// Remove the new, partially constructed transaction from beef as it will never be a valid transaction.
ab.txs.slice(ab.txs.length - 1)
this.beef.mergeBeefFromParty(this.storageParty, ab)
} else if (r.tx) {
this.beef.mergeBeefFromParty(this.storageParty, r.tx)
}
return r
}
async signAction(args: SignActionArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<SignActionResult> {
sdk.validateOriginator(originator)
const { auth, vargs } = this.validateAuthAndArgs(args, sdk.validateSignActionArgs)
const r = await signAction(this, auth, vargs)
return r
}
async abortAction(args: AbortActionArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<AbortActionResult> {
sdk.validateOriginator(originator)
const { auth } = this.validateAuthAndArgs(args, sdk.validateAbortActionArgs)
const r = await this.storage.abortAction(args)
return r
}
async internalizeAction(args: InternalizeActionArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<InternalizeActionResult> {
sdk.validateOriginator(originator)
const { auth, vargs } = this.validateAuthAndArgs(args, sdk.validateInternalizeActionArgs)
const r = await internalizeAction(this, auth, args)
return r
}
async relinquishOutput(args: RelinquishOutputArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<RelinquishOutputResult> {
sdk.validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, sdk.validateRelinquishOutputArgs)
const r = await this.storage.relinquishOutput(args)
return { relinquished: true }
}
async isAuthenticated(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<AuthenticatedResult> {
sdk.validateOriginator(originator)
const r: { authenticated: true; } = {
authenticated: true
}
return r
}
async waitForAuthentication(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<AuthenticatedResult> {
sdk.validateOriginator(originator)
return { authenticated: true }
}
async getHeight(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<GetHeightResult> {
sdk.validateOriginator(originator)
const height = await this.getServices().getHeight()
return { height }
}
async getHeaderForHeight(args: GetHeaderArgs, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<GetHeaderResult> {
sdk.validateOriginator(originator)
const serializedHeader = await this.getServices().getHeaderForHeight(args.height)
return { header: Utils.toHex(serializedHeader) }
}
async getNetwork(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<GetNetworkResult> {
sdk.validateOriginator(originator)
return { network: toWalletNetwork(this.chain) }
}
async getVersion(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes)
: Promise<GetVersionResult> {
sdk.validateOriginator(originator)
return { version: 'wallet-brc100-1.0.0' }
}
}
export interface PendingStorageInput {
vin: number,
derivationPrefix: string,
derivationSuffix: string,
unlockerPubKey?: string,
sourceSatoshis: number,
lockingScript: string
}
export interface PendingSignAction {
reference: string
dcr: sdk.StorageCreateActionResult
args: sdk.ValidCreateActionArgs
tx: BsvTransaction
amount: number
pdi: PendingStorageInput[]
}