@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
1,135 lines (1,009 loc) • 39.2 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,
createNonce,
AuthFetch,
verifyNonce,
MasterCertificate,
Certificate,
LookupResolver,
AtomicBEEF,
BEEF,
KeyDeriverApi
} from '@bsv/sdk'
import { acquireDirectCertificate } from './signer/methods/acquireDirectCertificate'
import { proveCertificate } from './signer/methods/proveCertificate'
import { createAction, CreateActionResultX } from './signer/methods/createAction'
import { signAction, SignActionResultX } from './signer/methods/signAction'
import { internalizeAction } from './signer/methods/internalizeAction'
import { WalletSettingsManager } from './WalletSettingsManager'
import { queryOverlay, transformVerifiableCertificatesWithTrust } from './utility/identityUtils'
import { maxPossibleSatoshis } from './storage/methods/generateChange'
import { WalletStorageManager } from './storage/WalletStorageManager'
import { Monitor } from './monitor/Monitor'
import { WalletSigner } from './signer/WalletSigner'
import { randomBytesBase64, toWalletNetwork } from './utility/utilityHelpers'
import { ScriptTemplateBRC29 } from './utility/ScriptTemplateBRC29'
import {
Chain,
KeyPair,
specOpFailedActions,
specOpInvalidChange,
specOpNoSendActions,
specOpSetWalletChangeParams,
specOpThrowReviewActions,
specOpWalletBalance,
StorageIdentity,
WalletBalance
} from './sdk/types'
import { WalletServices } from './sdk/WalletServices.interfaces'
import { PrivilegedKeyManager } from './sdk/PrivilegedKeyManager'
import { WERR_INTERNAL, WERR_INVALID_PARAMETER, WERR_REVIEW_ACTIONS } from './sdk/WERR_errors'
import {
validateAbortActionArgs,
validateAcquireDirectCertificateArgs,
validateAcquireIssuanceCertificateArgs,
validateCreateActionArgs,
validateDiscoverByAttributesArgs,
validateDiscoverByIdentityKeyArgs,
validateInternalizeActionArgs,
validateListActionsArgs,
validateListCertificatesArgs,
validateListOutputsArgs,
validateOriginator,
validateProveCertificateArgs,
validateRelinquishCertificateArgs,
validateRelinquishOutputArgs,
validateSignActionArgs,
ValidCreateActionArgs,
ValidWalletSignerArgs
} from './sdk/validationHelpers'
import { AuthId, StorageCreateActionResult, StorageInternalizeActionResult } from './sdk/WalletStorage.interfaces'
import { WalletError } from './sdk/WalletError'
export interface WalletArgs {
chain: Chain
keyDeriver: KeyDeriverApi
storage: WalletStorageManager
services?: WalletServices
monitor?: Monitor
privilegedKeyManager?: PrivilegedKeyManager
settingsManager?: WalletSettingsManager
lookupResolver?: LookupResolver
}
function isWalletSigner(args: WalletArgs | WalletSigner): args is WalletSigner {
return args['isWalletSigner']
}
export class Wallet implements WalletInterface, ProtoWallet {
chain: Chain
keyDeriver: KeyDeriverApi
storage: WalletStorageManager
settingsManager: WalletSettingsManager
lookupResolver: LookupResolver
services?: WalletServices
monitor?: Monitor
identityKey: string
/**
* The wallet creates a `BeefParty` when it is created.
* All the Beefs that pass through the wallet are merged into this beef.
* Thus what it contains at any time is the union of all transactions and proof data processed.
* The class `BeefParty` derives from `Beef`, adding the ability to track the source of merged data.
*
* This allows it to generate beefs to send to a particular “party” (storage or the user)
* that includes “txid only proofs” for transactions they already know about.
* Over time, this allows an active wallet to drastically reduce the amount of data transmitted.
*/
beef: BeefParty
/**
* If true, signableTransactions will include sourceTransaction for each input,
* including those that do not require signature and those that were also contained
* in the inputBEEF.
*/
includeAllSourceTransactions: boolean = true
/**
* If true, txids that are known to the wallet's party beef do not need to be returned from storage.
*/
autoKnownTxids: boolean = false
/**
* If true, beefs returned to the user may contain txidOnly transactions.
*/
returnTxidOnly: boolean = false
trustSelf?: TrustSelf
userParty: string
proto: ProtoWallet
privilegedKeyManager?: PrivilegedKeyManager
pendingSignActions: Record<string, PendingSignAction>
/**
* For repeatability testing, set to an array of random numbers from [0..1).
*/
randomVals?: number[] = undefined
constructor(
argsOrSigner: WalletArgs | WalletSigner,
services?: WalletServices,
monitor?: Monitor,
privilegedKeyManager?: 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 WERR_INVALID_PARAMETER(
'storage',
`authenticated as the same identityKey (${args.storage._authId.identityKey}) as the keyDeriver (${args.keyDeriver.identityKey}).`
)
this.settingsManager = args.settingsManager || new WalletSettingsManager(this)
this.chain = args.chain
this.lookupResolver =
args.lookupResolver ||
new LookupResolver({
networkPreset: toWalletNetwork(this.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(): KeyPair {
const kp: 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(): WalletServices {
if (!this.services)
throw new 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(): StorageIdentity {
const s = this.storage.getSettings()
return {
storageIdentityKey: s.storageIdentityKey,
storageName: s.storageName
}
}
private validateAuthAndArgs<A, T extends ValidWalletSignerArgs>(
args: A,
validate: (args: A) => T
): { vargs: T; auth: AuthId } {
const vargs = validate(args)
const auth: AuthId = { identityKey: this.identityKey }
return { vargs, auth }
}
//////////////////
// List Methods
//////////////////
async listActions(
args: ListActionsArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<ListActionsResult> {
validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, 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> {
validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, validateListOutputsArgs)
if (this.autoKnownTxids && !vargs.knownTxids) {
vargs.knownTxids = this.getKnownTxids()
}
const r = await this.storage.listOutputs(vargs)
if (r.BEEF) {
this.beef.mergeBeefFromParty(this.storageParty, r.BEEF)
r.BEEF = this.verifyReturnedTxidOnlyBEEF(r.BEEF)
}
return r
}
async listCertificates(
args: ListCertificatesArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<ListCertificatesResult> {
validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, validateListCertificatesArgs)
const r = await this.storage.listCertificates(vargs)
return r
}
//////////////////
// Certificates
//////////////////
async acquireCertificate(
args: AcquireCertificateArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<AcquireCertificateResult> {
validateOriginator(originator)
if (args.acquisitionProtocol === 'direct') {
const { auth, vargs } = this.validateAuthAndArgs(args, 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...
// TODO: Clean up MasterCertificate to support decrypt on instance
const cert = new MasterCertificate(
vargs.type,
vargs.serialNumber,
vargs.subject,
vargs.certifier,
vargs.revocationOutpoint,
vargs.fields,
vargs.keyringForSubject,
vargs.signature
)
await cert.verify()
// Verify certificate details
await MasterCertificate.decryptFields(
this,
vargs.keyringForSubject,
vargs.fields,
vargs.certifier,
vargs.privileged,
vargs.privilegedReason
)
} catch (eu: unknown) {
const e = WalletError.fromUnknown(eu)
throw new 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') {
const { auth, vargs } = this.validateAuthAndArgs(args, validateAcquireIssuanceCertificateArgs)
// Create a random nonce that the server can verify
const clientNonce = await createNonce(this, vargs.certifier)
// TODO: Consider adding support to request certificates from a certifier before acquiring a certificate.
const authClient = new AuthFetch(this)
// Create a certificate master keyring
// The certifier is able to decrypt these fields as they are the counterparty
const { certificateFields, masterKeyring } = await MasterCertificate.createCertificateFields(
this,
vargs.certifier,
vargs.fields
)
// Make a Certificate Signing Request (CSR) to the certifier
const response = await authClient.fetch(`${vargs.certifierUrl}/signCertificate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
clientNonce,
type: vargs.type,
fields: certificateFields,
masterKeyring
})
})
if (response.headers.get('x-bsv-auth-identity-key') !== vargs.certifier) {
throw new Error(
`Invalid certifier! Expected: ${vargs.certifier}, Received: ${response.headers.get('x-bsv-auth-identity-key')}`
)
}
const { certificate, serverNonce } = await response.json()
// Validate the server response
if (!certificate) {
throw new Error('No certificate received from certifier!')
}
if (!serverNonce) {
throw new Error('No serverNonce received from certifier!')
}
const signedCertificate = new Certificate(
certificate.type,
certificate.serialNumber,
certificate.subject,
certificate.certifier,
certificate.revocationOutpoint,
certificate.fields,
certificate.signature
)
// Validate server nonce
await verifyNonce(serverNonce, this, vargs.certifier)
// Verify the server included our nonce
const { valid } = await this.verifyHmac({
hmac: Utils.toArray(signedCertificate.serialNumber, 'base64'),
data: Utils.toArray(clientNonce + serverNonce, 'base64'),
protocolID: [2, 'certificate issuance'],
keyID: serverNonce + clientNonce,
counterparty: vargs.certifier
})
if (!valid) throw new Error('Invalid serialNumber')
// Validate the certificate received
if (signedCertificate.type !== vargs.type) {
throw new Error(`Invalid certificate type! Expected: ${vargs.type}, Received: ${signedCertificate.type}`)
}
if (signedCertificate.subject !== this.identityKey) {
throw new Error(
`Invalid certificate subject! Expected: ${this.identityKey}, Received: ${signedCertificate.subject}`
)
}
if (signedCertificate.certifier !== vargs.certifier) {
throw new Error(`Invalid certifier! Expected: ${vargs.certifier}, Received: ${signedCertificate.certifier}`)
}
if (!signedCertificate.revocationOutpoint) {
throw new Error(`Invalid revocationOutpoint!`)
}
if (Object.keys(signedCertificate.fields).length !== Object.keys(certificateFields).length) {
throw new Error(`Fields mismatch! Objects have different numbers of keys.`)
}
for (const field of Object.keys(certificateFields)) {
if (!(field in signedCertificate.fields)) {
throw new Error(`Missing field: ${field} in certificate.fields`)
}
if (signedCertificate.fields[field] !== certificateFields[field]) {
throw new Error(
`Invalid field! Expected: ${certificateFields[field]}, Received: ${signedCertificate.fields[field]}`
)
}
}
await signedCertificate.verify()
// Test decryption works
await MasterCertificate.decryptFields(this, masterKeyring, certificate.fields, vargs.certifier)
// Store the newly issued certificate
return await acquireDirectCertificate(this, auth, {
...certificate,
keyringRevealer: 'certifier',
keyringForSubject: masterKeyring,
privileged: vargs.privileged
})
}
throw new WERR_INVALID_PARAMETER('acquisitionProtocol', `valid.${args.acquisitionProtocol} is unrecognized.`)
}
async relinquishCertificate(
args: RelinquishCertificateArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<RelinquishCertificateResult> {
validateOriginator(originator)
this.validateAuthAndArgs(args, validateRelinquishCertificateArgs)
const r = await this.storage.relinquishCertificate(args)
return { relinquished: true }
}
async proveCertificate(
args: ProveCertificateArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<ProveCertificateResult> {
originator = validateOriginator(originator)
const { auth, vargs } = this.validateAuthAndArgs(args, validateProveCertificateArgs)
const r = await proveCertificate(this, auth, vargs)
return r
}
/** 2-minute cache of trust settings for identity resolution paths */
private _trustSettingsCache?: {
expiresAt: number
trustSettings: Awaited<ReturnType<WalletSettingsManager['get']>>['trustSettings']
}
/** 2-minute cache of queryOverlay() results keyed by normalized query */
private _overlayCache: Map<string, { expiresAt: number; value: unknown }> = new Map()
async discoverByIdentityKey(
args: DiscoverByIdentityKeyArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<DiscoverCertificatesResult> {
validateOriginator(originator)
this.validateAuthAndArgs(args, validateDiscoverByIdentityKeyArgs)
const TTL_MS = 2 * 60 * 1000
const now = Date.now()
// --- trustSettings cache (2 minutes) ---
let trustSettings =
this._trustSettingsCache && this._trustSettingsCache.expiresAt > now
? this._trustSettingsCache.trustSettings
: undefined
if (!trustSettings) {
const settings = await this.settingsManager.get()
trustSettings = settings.trustSettings
this._trustSettingsCache = { trustSettings, expiresAt: now + TTL_MS }
}
const certifiers = trustSettings.trustedCertifiers.map(c => c.identityKey).sort()
// --- queryOverlay cache (2 minutes) ---
const cacheKey = JSON.stringify({
fn: 'discoverByIdentityKey',
identityKey: args.identityKey,
certifiers
})
let cached = this._overlayCache.get(cacheKey)
if (!cached || cached.expiresAt <= now) {
const value = await queryOverlay({ identityKey: args.identityKey, certifiers }, this.lookupResolver)
cached = { value, expiresAt: now + TTL_MS }
this._overlayCache.set(cacheKey, cached)
}
if (!cached.value) {
return { totalCertificates: 0, certificates: [] }
}
return transformVerifiableCertificatesWithTrust(trustSettings, cached.value as any)
}
async discoverByAttributes(
args: DiscoverByAttributesArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<DiscoverCertificatesResult> {
validateOriginator(originator)
this.validateAuthAndArgs(args, validateDiscoverByAttributesArgs)
const TTL_MS = 2 * 60 * 1000
const now = Date.now()
// --- trustSettings cache (2 minutes) ---
let trustSettings =
this._trustSettingsCache && this._trustSettingsCache.expiresAt > now
? this._trustSettingsCache.trustSettings
: undefined
if (!trustSettings) {
const settings = await this.settingsManager.get()
trustSettings = settings.trustSettings
this._trustSettingsCache = { trustSettings, expiresAt: now + TTL_MS }
}
const certifiers = trustSettings.trustedCertifiers.map(c => c.identityKey).sort()
// Normalize attributes for a stable cache key.
// If attributes is an object, sort its top-level keys; if it's an array, sort a shallow copy.
let attributesKey: unknown = args.attributes
if (args.attributes && typeof args.attributes === 'object') {
const keys = Object.keys(args.attributes as Record<string, unknown>).sort()
attributesKey = JSON.stringify(args.attributes, keys)
}
// --- queryOverlay cache (2 minutes) ---
const cacheKey = JSON.stringify({
fn: 'discoverByAttributes',
attributes: attributesKey,
certifiers
})
let cached = this._overlayCache.get(cacheKey)
if (!cached || cached.expiresAt <= now) {
const value = await queryOverlay({ attributes: args.attributes, certifiers }, this.lookupResolver)
cached = { value, expiresAt: now + TTL_MS }
this._overlayCache.set(cacheKey, cached)
}
if (!cached.value) {
return { totalCertificates: 0, certificates: [] }
}
return transformVerifiableCertificatesWithTrust(trustSettings, cached.value as any)
}
verifyReturnedTxidOnly(beef: Beef, knownTxids?: string[]): Beef {
if (this.returnTxidOnly) return beef
const onlyTxids = beef.txs.filter(btx => btx.isTxidOnly).map(btx => btx.txid)
for (const txid of onlyTxids) {
if (knownTxids && knownTxids.indexOf(txid) >= 0) continue
const btx = beef.findTxid(txid)
const tx = this.beef.findAtomicTransaction(txid)
if (!tx) throw new WERR_INTERNAL(`unable to merge txid ${txid} into beef`)
beef.mergeTransaction(tx)
}
for (const btx of beef.txs) {
if (knownTxids && knownTxids.indexOf(btx.txid) >= 0) continue
if (btx.isTxidOnly) throw new WERR_INTERNAL(`remaining txidOnly ${btx.txid} is not known`)
}
return beef
}
verifyReturnedTxidOnlyAtomicBEEF(beef: AtomicBEEF, knownTxids?: string[]): AtomicBEEF {
if (this.returnTxidOnly) return beef
const b = Beef.fromBinary(beef)
if (!b.atomicTxid) throw new WERR_INTERNAL()
return this.verifyReturnedTxidOnly(b, knownTxids).toBinaryAtomic(b.atomicTxid!)
}
verifyReturnedTxidOnlyBEEF(beef: BEEF): BEEF {
if (this.returnTxidOnly) return beef
const b = Beef.fromBinary(beef)
return this.verifyReturnedTxidOnly(b).toBinary()
}
//////////////////
// Actions
//////////////////
async createAction(
args: CreateActionArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<CreateActionResult> {
validateOriginator(originator)
if (!args.options) args.options = {}
args.options.trustSelf ||= this.trustSelf
if (this.autoKnownTxids && !args.options.knownTxids) {
args.options.knownTxids = this.getKnownTxids(args.options.knownTxids)
}
const { auth, vargs } = this.validateAuthAndArgs(args, validateCreateActionArgs)
if (vargs.labels.indexOf(specOpThrowReviewActions) >= 0) throwDummyReviewActions()
vargs.includeAllSourceTransactions = this.includeAllSourceTransactions
if (this.randomVals && this.randomVals.length > 1) {
vargs.randomVals = [...this.randomVals]
}
const r = await createAction(this, auth, vargs)
if (r.tx) {
this.beef.mergeBeefFromParty(this.storageParty, r.tx)
}
if (r.tx) r.tx = this.verifyReturnedTxidOnlyAtomicBEEF(r.tx, args.options?.knownTxids)
if (!vargs.isDelayed) throwIfAnyUnsuccessfulCreateActions(r)
return r
}
async signAction(
args: SignActionArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<SignActionResult> {
validateOriginator(originator)
const { auth, vargs } = this.validateAuthAndArgs(args, validateSignActionArgs)
// createAction options are merged with undefined signAction options before validation...
const r = await signAction(this, auth, args)
if (!vargs.isDelayed) throwIfAnyUnsuccessfulSignActions(r)
const prior = this.pendingSignActions[args.reference]
if (r.tx) r.tx = this.verifyReturnedTxidOnlyAtomicBEEF(r.tx, prior.args.options?.knownTxids)
return r
}
async internalizeAction(
args: InternalizeActionArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<InternalizeActionResult> {
validateOriginator(originator)
const { auth, vargs } = this.validateAuthAndArgs(args, validateInternalizeActionArgs)
if (vargs.labels.indexOf(specOpThrowReviewActions) >= 0) throwDummyReviewActions()
const r = await internalizeAction(this, auth, args)
throwIfUnsuccessfulInternalizeAction(r)
return r
}
async abortAction(
args: AbortActionArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<AbortActionResult> {
validateOriginator(originator)
const { auth } = this.validateAuthAndArgs(args, validateAbortActionArgs)
const r = await this.storage.abortAction(args)
return r
}
async relinquishOutput(
args: RelinquishOutputArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<RelinquishOutputResult> {
validateOriginator(originator)
const { vargs } = this.validateAuthAndArgs(args, validateRelinquishOutputArgs)
const r = await this.storage.relinquishOutput(args)
return { relinquished: true }
}
async isAuthenticated(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<AuthenticatedResult> {
validateOriginator(originator)
const r: { authenticated: true } = {
authenticated: true
}
return r
}
async waitForAuthentication(
args: {},
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<AuthenticatedResult> {
validateOriginator(originator)
return { authenticated: true }
}
async getHeight(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetHeightResult> {
validateOriginator(originator)
const height = await this.getServices().getHeight()
return { height }
}
async getHeaderForHeight(
args: GetHeaderArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<GetHeaderResult> {
validateOriginator(originator)
const serializedHeader = await this.getServices().getHeaderForHeight(args.height)
return { header: Utils.toHex(serializedHeader) }
}
async getNetwork(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetNetworkResult> {
validateOriginator(originator)
return { network: toWalletNetwork(this.chain) }
}
async getVersion(args: {}, originator?: OriginatorDomainNameStringUnder250Bytes): Promise<GetVersionResult> {
validateOriginator(originator)
return { version: 'wallet-brc100-1.0.0' }
}
/**
* Transfer all possible satoshis held by this wallet to `toWallet`.
*
* @param toWallet wallet which will receive this wallet's satoshis.
*/
async sweepTo(toWallet: Wallet): Promise<void> {
const derivationPrefix = randomBytesBase64(8)
const derivationSuffix = randomBytesBase64(8)
const keyDeriver = this.keyDeriver
const t = new ScriptTemplateBRC29({
derivationPrefix,
derivationSuffix,
keyDeriver
})
const label = 'sweep'
const satoshis = maxPossibleSatoshis
const car = await this.createAction({
outputs: [
{
lockingScript: t.lock(keyDeriver.rootKey.toString(), toWallet.identityKey).toHex(),
satoshis,
outputDescription: label,
tags: ['relinquish'],
customInstructions: JSON.stringify({
derivationPrefix,
derivationSuffix,
type: 'BRC29'
})
}
],
options: {
randomizeOutputs: false,
acceptDelayedBroadcast: false
},
labels: [label],
description: label
})
const iar = await toWallet.internalizeAction({
tx: car.tx!,
outputs: [
{
outputIndex: 0,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix,
derivationSuffix,
senderIdentityKey: this.identityKey
}
}
],
description: label,
labels: [label]
})
}
/**
* Uses `listOutputs` to iterate over chunks of up to 1000 outputs to
* compute the sum of output satoshis.
*
* @param {string} basket - Optional. Defaults to 'default', the wallet change basket.
* @returns {WalletBalance} total sum of output satoshis and utxo details (satoshis and outpoints)
*/
async balanceAndUtxos(basket: string = 'default'): Promise<WalletBalance> {
const r: WalletBalance = { total: 0, utxos: [] }
let offset = 0
for (;;) {
const change = await this.listOutputs({
basket,
limit: 1000,
offset
})
if (change.totalOutputs === 0) break
for (const o of change.outputs) {
r.total += o.satoshis
r.utxos.push({ satoshis: o.satoshis, outpoint: o.outpoint })
}
offset += change.outputs.length
}
return r
}
/**
* Uses `listOutputs` special operation to compute the total value (of satoshis) for
* all spendable outputs in the 'default' basket.
*
* @returns {number} sum of output satoshis
*/
async balance(): Promise<number> {
const args: ListOutputsArgs = {
basket: specOpWalletBalance
}
const r = await this.listOutputs(args)
return r.totalOutputs
}
/**
* Uses `listOutputs` special operation to review the spendability via `Services` of
* outputs currently considered spendable. Returns the outputs that fail to verify.
*
* Ignores the `limit` and `offset` properties.
*
* @param all Defaults to false. If false, only change outputs ('default' basket) are reviewed. If true, all spendable outputs are reviewed.
* @param release Defaults to false. If true, sets outputs that fail to verify to un-spendable (spendable: false)
* @param optionalArgs Optional. Additional tags will constrain the outputs processed.
* @returns outputs which are/where considered spendable but currently fail to verify as spendable.
*/
async reviewSpendableOutputs(
all = false,
release = false,
optionalArgs?: Partial<ListOutputsArgs>
): Promise<ListOutputsResult> {
const args: ListOutputsArgs = {
...(optionalArgs || {}),
basket: specOpInvalidChange
}
args.tags ||= []
if (all) args.tags.push('all')
if (release) args.tags.push('release')
const r = await this.listOutputs(args)
return r
}
/**
* Uses `listOutputs` special operation to update the 'default' basket's automatic
* change generation parameters.
*
* @param count target number of change UTXOs to maintain.
* @param satoshis target value for new change outputs.
*/
async setWalletChangeParams(count: number, satoshis: number): Promise<void> {
const args: ListOutputsArgs = {
basket: specOpSetWalletChangeParams,
tags: [count.toString(), satoshis.toString()]
}
await this.listOutputs(args)
}
/**
* Uses `listActions` special operation to return only actions with status 'nosend'.
*
* @param abort Defaults to false. If true, runs `abortAction` on each 'nosend' action.
* @returns {ListActionsResult} start `listActions` result restricted to 'nosend' (or 'failed' if aborted) actions.
*/
async listNoSendActions(args: ListActionsArgs, abort = false): Promise<ListActionsResult> {
const { vargs } = this.validateAuthAndArgs(args, validateListActionsArgs)
vargs.labels.push(specOpNoSendActions)
if (abort) vargs.labels.push('abort')
const r = await this.storage.listActions(vargs)
return r
}
/**
* Uses `listActions` special operation to return only actions with status 'failed'.
*
* @param unfail Defaults to false. If true, queues the action for attempted recovery.
* @returns {ListActionsResult} start `listActions` result restricted to 'failed' status actions.
*/
async listFailedActions(args: ListActionsArgs, unfail = false): Promise<ListActionsResult> {
const { vargs } = this.validateAuthAndArgs(args, validateListActionsArgs)
vargs.labels.push(specOpFailedActions)
if (unfail) vargs.labels.push('unfail')
const r = await this.storage.listActions(vargs)
return r
}
}
export interface PendingStorageInput {
vin: number
derivationPrefix: string
derivationSuffix: string
unlockerPubKey?: string
sourceSatoshis: number
lockingScript: string
}
export interface PendingSignAction {
reference: string
dcr: StorageCreateActionResult
args: ValidCreateActionArgs
tx: BsvTransaction
amount: number
pdi: PendingStorageInput[]
}
function throwIfAnyUnsuccessfulCreateActions(r: CreateActionResultX) {
const ndrs = r.notDelayedResults
const swrs = r.sendWithResults
if (!ndrs || !swrs || swrs.every(r => r.status === 'unproven')) return
throw new WERR_REVIEW_ACTIONS(ndrs, swrs, r.txid, r.tx, r.noSendChange)
}
function throwIfAnyUnsuccessfulSignActions(r: SignActionResultX) {
const ndrs = r.notDelayedResults
const swrs = r.sendWithResults
if (!ndrs || !swrs || swrs.every(r => r.status === 'unproven')) return
throw new WERR_REVIEW_ACTIONS(ndrs, swrs, r.txid, r.tx)
}
function throwIfUnsuccessfulInternalizeAction(r: StorageInternalizeActionResult) {
const ndrs = r.notDelayedResults
const swrs = r.sendWithResults
if (!ndrs || !swrs || swrs.every(r => r.status === 'unproven')) return
throw new WERR_REVIEW_ACTIONS(ndrs, swrs, r.txid)
}
/**
* Throws a WERR_REVIEW_ACTIONS with a full set of properties to test data formats and propagation.
*/
function throwDummyReviewActions() {
const b58Beef =
'gno9MC7VXii1KoCkc2nsVyYJpqzN3dhBzYATETJcys62emMKfpBof4R7GozwYEaSapUtnNvqQ57aaYYjm3U2dv9eUJ1sV46boHkQgppYmAz9YH8FdZduV8aJayPViaKcyPmbDhEw6UW8TM5iFZLXNs7HBnJHUKCeTdNK4FUEL7vAugxAV9WUUZ43BZjJk2SmSeps9TCXjt1Ci9fKWp3d9QSoYvTpxwzyUFHjRKtbUgwq55ZfkBp5bV2Bpz9qSuKywKewW7Hh4S1nCUScwwzpKDozb3zic1V9p2k8rQxoPsRxjUJ8bjhNDdsN8d7KukFuc3n47fXzdWttvnxwsujLJRGnQbgJuknQqx3KLf5kJXHzwjG6TzigZk2t24qeB6d3hbYiaDr2fFkUJBL3tukTHhfNkQYRXuz3kucVDzvejHyqJaF51mXG8BjMN5aQj91ZJXCaPVqkMWCzmvyaqmXMdRiJdSAynhXbQK91xf6RwdNhz1tg5f9B6oJJMhsi9UYSVymmax8VLKD9AKzBCBDcfyD83m3jyS1VgKGZn3SkQmr6bsoWq88L3GsMnnmYUGogvdAYarTqg3pzkjCMxHzmJBMN6ofnUk8c1sRTXQue7BbyUaN5uZu3KW6CmFsEfpuqVvnqFW93TU1jrPP2S8yz8AexAnARPCKE8Yz7RfVaT6RCavwQKL3u5iookwRWEZXW1QWmM37yJWHD87SjVynyg327a1CLwcBxmE2CB48QeNVGyQki4CTQMqw2o8TMhDPJej1g68oniAjBcxBLSCs7KGvK3k7AfrHbCMULX9CTibYhCjdFjbsbBoocqJpxxcvkMo1fEEiAzZuiBVZQDYktDdTVbhKHvYkW25HcYX75NJrpNAhm7AjFeKLzEVxqAQkMfvTufpESNRZF4kQqg2Rg8h2ajcKTd5cpEPwXCrZLHm4EaZEmZVbg3QNfGhn7BJu1bHMtLqPD4y8eJxm2uGrW6saf6qKYmmu64F8A667NbD4yskPRQ1S863VzwGpxxmgLc1Ta3R46jEqsAoRDoZVUaCgBBZG3Yg1CTgi1EVBMXU7qvY4n3h8o2FLCEMWY4KadnV3iD4FbcdCmg4yxBosNAZgbPjhgGjCimjh4YsLd9zymGLmivmz2ZBg5m3xaiXT9NN81X9C1JUujd'
const beef = Beef.fromBinary(Utils.fromBase58(b58Beef))
const btx = beef.txs.slice(-1)[0]
const txid = btx.txid
debugger
throw new WERR_REVIEW_ACTIONS(
[
{
txid, // only care that it is syntactically a txid
status: 'doubleSpend',
competingTxs: [txid], // a txid in the beef
competingBeef: beef.toBinary()
}
],
[
{
txid,
status: 'failed'
}
],
txid,
beef.toBinaryAtomic(txid),
[`${txid}.0`]
)
}