@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
175 lines (146 loc) • 5.68 kB
text/typescript
import {
AtomicBEEF,
Beef,
CreateActionResult,
OutpointString,
SendWithResult,
SignableTransaction,
TXIDHexString
} from '@bsv/sdk'
import { Script, Transaction } from '@bsv/sdk'
import { makeAtomicBeef, PendingSignAction, ScriptTemplateBRC29, sdk, verifyTruthy, Wallet } from '../../index.client'
import { buildSignableTransaction } from './buildSignableTransaction'
import { ReviewActionResult } from '../../sdk/WalletStorage.interfaces'
import { completeSignedTransaction, verifyUnlockScripts } from './completeSignedTransaction'
export interface CreateActionResultX extends CreateActionResult {
txid?: TXIDHexString
tx?: AtomicBEEF
noSendChange?: OutpointString[]
sendWithResults?: SendWithResult[]
signableTransaction?: SignableTransaction
notDelayedResults?: ReviewActionResult[]
}
export async function createAction(
wallet: Wallet,
auth: sdk.AuthId,
vargs: sdk.ValidCreateActionArgs
): Promise<CreateActionResultX> {
const r: CreateActionResultX = {}
let prior: PendingSignAction | undefined = undefined
if (vargs.isNewTx) {
prior = await createNewTx(wallet, vargs)
if (vargs.isSignAction) {
return makeSignableTransactionResult(prior, wallet, vargs)
}
prior.tx = await completeSignedTransaction(prior, {}, wallet)
r.txid = prior.tx.id('hex')
const beef = new Beef()
if (prior.dcr.inputBeef) beef.mergeBeef(prior.dcr.inputBeef)
beef.mergeTransaction(prior.tx)
verifyUnlockScripts(r.txid, beef)
r.noSendChange = prior.dcr.noSendChangeOutputVouts?.map(vout => `${r.txid}.${vout}`)
if (!vargs.options.returnTXIDOnly) r.tx = beef.toBinaryAtomic(r.txid)
}
const { sendWithResults, notDelayedResults } = await processAction(prior, wallet, auth, vargs)
r.sendWithResults = sendWithResults
r.notDelayedResults = notDelayedResults
return r
}
async function createNewTx(wallet: Wallet, args: sdk.ValidCreateActionArgs): Promise<PendingSignAction> {
const storageArgs = removeUnlockScripts(args)
const dcr = await wallet.storage.createAction(storageArgs)
const reference = dcr.reference
const { tx, amount, pdi } = buildSignableTransaction(dcr, args, wallet)
const prior: PendingSignAction = { reference, dcr, args, amount, tx, pdi }
return prior
}
function makeSignableTransactionResult(
prior: PendingSignAction,
wallet: Wallet,
args: sdk.ValidCreateActionArgs
): CreateActionResult {
if (!prior.dcr.inputBeef) throw new sdk.WERR_INTERNAL('prior.dcr.inputBeef must be valid')
const txid = prior.tx.id('hex')
const r: CreateActionResult = {
noSendChange: args.isNoSend ? prior.dcr.noSendChangeOutputVouts?.map(vout => `${txid}.${vout}`) : undefined,
signableTransaction: {
reference: prior.dcr.reference,
tx: makeSignableTransactionBeef(prior.tx, prior.dcr.inputBeef)
}
}
wallet.pendingSignActions[r.signableTransaction!.reference] = prior
return r
}
function makeSignableTransactionBeef(tx: Transaction, inputBEEF: number[]): number[] {
// This is a special case beef for transaction signing.
// We only need the transaction being signed, and for each input, the raw source transaction.
const beef = new Beef()
for (const input of tx.inputs) {
if (!input.sourceTransaction)
throw new sdk.WERR_INTERNAL('Every signableTransaction input must have a sourceTransaction')
beef.mergeRawTx(input.sourceTransaction!.toBinary())
}
beef.mergeRawTx(tx.toBinary())
return beef.toBinaryAtomic(tx.id('hex'))
}
/**
* Derive a change output locking script
*/
export function makeChangeLock(
out: sdk.StorageCreateTransactionSdkOutput,
dctr: sdk.StorageCreateActionResult,
args: sdk.ValidCreateActionArgs,
changeKeys: sdk.KeyPair,
wallet: Wallet
): Script {
const derivationPrefix = dctr.derivationPrefix
const derivationSuffix = verifyTruthy(out.derivationSuffix)
const sabppp = new ScriptTemplateBRC29({
derivationPrefix,
derivationSuffix,
keyDeriver: wallet.keyDeriver
})
const lockingScript = sabppp.lock(changeKeys.privateKey, changeKeys.publicKey)
return lockingScript
}
function removeUnlockScripts(args: sdk.ValidCreateActionArgs) {
let storageArgs = args
if (!storageArgs.inputs.every(i => i.unlockingScript === undefined)) {
// Never send unlocking scripts to storage, all it needs is the script length.
storageArgs = { ...args, inputs: [] }
for (const i of args.inputs) {
const di: sdk.ValidCreateActionInput = {
...i,
unlockingScriptLength: i.unlockingScript !== undefined ? i.unlockingScript.length : i.unlockingScriptLength
}
delete di.unlockingScript
storageArgs.inputs.push(di)
}
}
return storageArgs
}
export async function processAction(
prior: PendingSignAction | undefined,
wallet: Wallet,
auth: sdk.AuthId,
vargs: sdk.ValidProcessActionArgs
): Promise<sdk.StorageProcessActionResults> {
const args: sdk.StorageProcessActionArgs = {
isNewTx: vargs.isNewTx,
isSendWith: vargs.isSendWith,
isNoSend: vargs.isNoSend,
isDelayed: vargs.isDelayed,
reference: prior ? prior.reference : undefined,
txid: prior ? prior.tx.id('hex') : undefined,
rawTx: prior ? prior.tx.toBinary() : undefined,
sendWith: vargs.isSendWith ? vargs.options.sendWith : []
}
const r: sdk.StorageProcessActionResults = await wallet.storage.processAction(args)
return r
}
function makeDummyTransactionForOutputSatoshis(vout: number, satoshis: number): Transaction {
const tx = new Transaction()
for (let i = 0; i < vout; i++) tx.addOutput({ lockingScript: new Script(), satoshis: 0 })
tx.addOutput({ lockingScript: new Script(), satoshis })
return tx
}