@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
950 lines (847 loc) • 30.5 kB
text/typescript
import {
Beef,
BigNumber,
Curve,
OriginatorDomainNameStringUnder250Bytes,
P2PKH,
PrivateKey,
PubKeyHex,
PublicKey,
Random,
Script,
Utils
} from '@bsv/sdk'
import {
asArray,
asString,
EntityProvenTx,
randomBytesBase64,
sdk,
sha256Hash,
StorageProvider,
TableOutput,
TableOutputBasket,
TableOutputTag,
TableTransaction,
validateStorageFeeModel,
verifyId,
verifyInteger,
verifyNumber,
verifyOne,
verifyOneOrNone,
verifyTruthy
} from '../../index.client'
import {
generateChangeSdk,
GenerateChangeSdkChangeInput,
GenerateChangeSdkParams,
maxPossibleSatoshis
} from './generateChange'
let disableDoubleSpendCheckForTest = true
export function setDisableDoubleSpendCheckForTest(v: boolean) {
disableDoubleSpendCheckForTest = v
}
export async function createAction(
storage: StorageProvider,
auth: sdk.AuthId,
vargs: sdk.ValidCreateActionArgs,
originator?: OriginatorDomainNameStringUnder250Bytes
): Promise<sdk.StorageCreateActionResult> {
//stampLog(vargs, `start storage createTransactionSdk`)
if (!vargs.isNewTx)
// The purpose of this function is to create the initial storage records associated
// with a new transaction. It's an error if we have no new inputs or outputs...
throw new sdk.WERR_INTERNAL()
/**
* Steps to create a transaction:
* - Verify that all inputs either have proof in vargs.inputBEEF or that options.trustSelf === 'known' and input txid.vout are known valid to storage.
* - Create a new transaction record with status 'unsigned' as the anchor for construction work and to new outputs.
* - Create all transaction labels.
* - Add new commission output
* - Attempt to fund the transaction by allocating change outputs:
* - As each change output is selected it is simultaneously locked.
* - Create all new output, basket, tag records
* - If requested, create result Beef with complete proofs for all inputs used
* - Create result inputs with source locking scripts
* - Create result outputs with new locking scripts.
* - Create and return result.
*/
const userId = auth.userId!
const { storageBeef, beef, xinputs } = await validateRequiredInputs(storage, userId, vargs)
const xoutputs = validateRequiredOutputs(storage, userId, vargs)
const changeBasketName = 'default'
const changeBasket = verifyOne(
await storage.findOutputBaskets({
partial: { userId, name: changeBasketName }
}),
`Invalid outputGeneration basket "${changeBasketName}"`
)
const noSendChangeIn = await validateNoSendChange(storage, userId, vargs, changeBasket)
const availableChangeCount = await storage.countChangeInputs(userId, changeBasket.basketId, !vargs.isDelayed)
const feeModel = validateStorageFeeModel(storage.feeModel)
const newTx = await createNewTxRecord(storage, userId, vargs, storageBeef)
const ctx: CreateTransactionSdkContext = {
xinputs,
xoutputs,
changeBasket,
noSendChangeIn,
availableChangeCount,
feeModel,
transactionId: newTx.transactionId!
}
const { allocatedChange, changeOutputs, derivationPrefix, maxPossibleSatoshisAdjustment } =
await fundNewTransactionSdk(storage, userId, vargs, ctx)
if (maxPossibleSatoshisAdjustment) {
const a = maxPossibleSatoshisAdjustment
if (ctx.xoutputs[a.fixedOutputIndex].satoshis !== maxPossibleSatoshis) throw new sdk.WERR_INTERNAL()
ctx.xoutputs[a.fixedOutputIndex].satoshis = a.satoshis
}
// The satoshis of the transaction is the satoshis we get back in change minus the satoshis we spend.
const satoshis =
changeOutputs.reduce((a, e) => a + e.satoshis!, 0) - allocatedChange.reduce((a, e) => a + e.satoshis!, 0)
await storage.updateTransaction(newTx.transactionId!, { satoshis })
const { outputs, changeVouts } = await createNewOutputs(storage, userId, vargs, ctx, changeOutputs)
const inputBeef = await mergeAllocatedChangeBeefs(storage, userId, vargs, allocatedChange, beef)
const inputs = await createNewInputs(storage, userId, vargs, ctx, allocatedChange)
const r: sdk.StorageCreateActionResult = {
reference: newTx.reference!,
version: newTx.version!,
lockTime: newTx.lockTime!,
inputs,
outputs,
derivationPrefix,
inputBeef,
noSendChangeOutputVouts: vargs.isNoSend ? changeVouts : undefined
}
//stampLog(vargs, `end storage createTransactionSdk`)
return r
}
interface CreateTransactionSdkContext {
xinputs: XValidCreateActionInput[]
xoutputs: XValidCreateActionOutput[]
changeBasket: TableOutputBasket
noSendChangeIn: TableOutput[]
availableChangeCount: number
feeModel: sdk.StorageFeeModel
transactionId: number
}
interface XValidCreateActionInput extends sdk.ValidCreateActionInput {
vin: number
lockingScript: Script
satoshis: number
output?: TableOutput
}
export interface XValidCreateActionOutput extends sdk.ValidCreateActionOutput {
vout: number
providedBy: sdk.StorageProvidedBy
purpose?: string
derivationSuffix?: string
keyOffset?: string
}
function makeDefaultOutput(userId: number, transactionId: number, satoshis: number, vout: number): TableOutput {
const now = new Date()
const output: TableOutput = {
created_at: now,
updated_at: now,
outputId: 0,
userId,
transactionId,
satoshis: satoshis,
vout,
basketId: undefined,
change: false,
customInstructions: undefined,
derivationPrefix: undefined,
derivationSuffix: undefined,
outputDescription: '',
lockingScript: undefined,
providedBy: 'you',
purpose: '',
senderIdentityKey: undefined,
spendable: true,
spendingDescription: undefined,
spentBy: undefined,
txid: undefined,
type: ''
}
return output
}
async function createNewInputs(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs,
ctx: CreateTransactionSdkContext,
allocatedChange: TableOutput[]
): Promise<sdk.StorageCreateTransactionSdkInput[]> {
const r: sdk.StorageCreateTransactionSdkInput[] = []
const newInputs: {
i?: XValidCreateActionInput
o?: TableOutput
unlockLen?: number
}[] = []
for (const i of ctx.xinputs) {
const o = i.output
newInputs.push({ i, o })
if (o) {
await storage.transaction(async trx => {
const o2 = verifyOne(await storage.findOutputs({ partial: { outputId: o.outputId }, trx }))
if (o2.spendable != true || o2.spentBy !== undefined)
throw new sdk.WERR_INVALID_PARAMETER(
`inputs[${i.vin}]`,
`spendable output. output ${o.txid}:${o.vout} appears to have been spent.`
)
await storage.updateOutput(
o.outputId!,
{
spendable: false,
spentBy: ctx.transactionId,
spendingDescription: i.inputDescription
},
trx
)
})
}
}
for (const o of allocatedChange) {
newInputs.push({ o, unlockLen: 107 })
}
let vin = -1
for (const { i, o, unlockLen } of newInputs) {
vin++
if (o) {
if (!i && !unlockLen) throw new sdk.WERR_INTERNAL(`vin ${vin} non-fixedInput without unlockLen`)
const sourceTransaction =
vargs.includeAllSourceTransactions && vargs.isSignAction
? await storage.getRawTxOfKnownValidTransaction(o.txid!)
: undefined
const ri: sdk.StorageCreateTransactionSdkInput = {
vin,
sourceTxid: o.txid!,
sourceVout: o!.vout!,
sourceSatoshis: o.satoshis!,
sourceLockingScript: asString(o.lockingScript!),
sourceTransaction,
unlockingScriptLength: unlockLen ? unlockLen : i!.unlockingScriptLength,
providedBy: i && o.providedBy === 'storage' ? 'you-and-storage' : (o.providedBy! as sdk.StorageProvidedBy),
type: o.type,
spendingDescription: o.spendingDescription || undefined,
derivationPrefix: o.derivationPrefix || undefined,
derivationSuffix: o.derivationSuffix || undefined,
senderIdentityKey: o.senderIdentityKey || undefined
}
r.push(ri)
} else {
if (!i) throw new sdk.WERR_INTERNAL(`vin ${vin} without output or xinput`)
// user specified input with no corresponding output being spent.
const ri: sdk.StorageCreateTransactionSdkInput = {
vin,
sourceTxid: i.outpoint.txid,
sourceVout: i.outpoint.vout,
sourceSatoshis: i.satoshis,
sourceLockingScript: i.lockingScript.toHex(),
unlockingScriptLength: i.unlockingScriptLength,
providedBy: 'you',
type: 'custom',
spendingDescription: undefined,
derivationPrefix: undefined,
derivationSuffix: undefined,
senderIdentityKey: undefined
}
r.push(ri)
}
}
return r
}
async function createNewOutputs(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs,
ctx: CreateTransactionSdkContext,
changeOutputs: TableOutput[]
): Promise<{
outputs: sdk.StorageCreateTransactionSdkOutput[]
changeVouts: number[]
}> {
const outputs: sdk.StorageCreateTransactionSdkOutput[] = []
// Lookup output baskets
const txBaskets: Record<string, TableOutputBasket> = {}
for (const xo of ctx.xoutputs) {
if (xo.basket !== undefined && !txBaskets[xo.basket])
txBaskets[xo.basket] = await storage.findOrInsertOutputBasket(userId, xo.basket!)
}
// Lookup output tags
const txTags: Record<string, TableOutputTag> = {}
for (const xo of ctx.xoutputs) {
for (const tag of xo.tags) {
txTags[tag] = await storage.findOrInsertOutputTag(userId, tag)
}
}
const newOutputs: { o: TableOutput; tags: string[] }[] = []
for (const xo of ctx.xoutputs) {
const lockingScript = asArray(xo.lockingScript)
if (xo.purpose === 'service-charge') {
const now = new Date()
await storage.insertCommission({
userId,
transactionId: ctx.transactionId,
lockingScript,
satoshis: xo.satoshis,
isRedeemed: false,
keyOffset: verifyTruthy(xo.keyOffset),
created_at: now,
updated_at: now,
commissionId: 0
})
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout)
o.lockingScript = lockingScript
o.providedBy = 'storage'
o.purpose = 'storage-commission'
o.type = 'custom'
o.spendable = false
newOutputs.push({ o, tags: [] })
} else {
// The user wants tracking if they put their output in a basket
const basketId = !xo.basket ? undefined : txBaskets[xo.basket].basketId!
const o = makeDefaultOutput(userId, ctx.transactionId, xo.satoshis, xo.vout)
o.lockingScript = lockingScript
o.basketId = basketId
o.customInstructions = xo.customInstructions
o.outputDescription = xo.outputDescription
o.providedBy = xo.providedBy
o.purpose = xo.purpose || ''
o.type = 'custom'
newOutputs.push({ o, tags: xo.tags })
}
}
for (const o of changeOutputs) {
o.spendable = true
newOutputs.push({ o, tags: [] })
}
if (vargs.options.randomizeOutputs) {
const randomVals: number[] = []
const nextRandomVal = (): number => {
let val = 0
if (!randomVals || randomVals.length === 0) {
val = Math.random()
} else {
val = randomVals.shift() || 0
randomVals.push(val)
}
return val
}
/** In-place array shuffle */
const shuffleArray = <T>(array: T[]): T[] => {
let currentIndex = array.length
let temporaryValue: T
let randomIndex: number
while (currentIndex !== 0) {
randomIndex = Math.floor(nextRandomVal() * currentIndex)
currentIndex -= 1
temporaryValue = array[currentIndex]
array[currentIndex] = array[randomIndex]
array[randomIndex] = temporaryValue
}
return array
}
let vout = -1
const newVouts = Array<number>(newOutputs.length)
for (let i = 0; i < newVouts.length; i++) newVouts[i] = i
shuffleArray(newVouts)
for (const no of newOutputs) {
vout++
if (no.o.vout !== vout) throw new sdk.WERR_INTERNAL(`new output ${vout} has out of order vout ${no.o.vout}`)
no.o.vout = newVouts[vout]
}
}
const changeVouts: number[] = []
for (const { o, tags } of newOutputs) {
o.outputId = await storage.insertOutput(o)
if (o.change && o.purpose === 'change' && o.providedBy === 'storage') changeVouts.push(o.vout!)
// Add tags to the output
for (const tagName of tags) {
const tag = txTags[tagName]!
await storage.findOrInsertOutputTagMap(verifyId(o.outputId), verifyId(tag.outputTagId))
}
const ro: sdk.StorageCreateTransactionSdkOutput = {
vout: verifyInteger(o.vout),
satoshis: verifyTruthy(o.satoshis),
lockingScript: !o.lockingScript ? '' : asString(o.lockingScript),
providedBy: verifyTruthy(o.providedBy) as sdk.StorageProvidedBy,
purpose: o.purpose || undefined,
basket: Object.values(txBaskets).find(b => b.basketId === o.basketId)?.name,
tags: tags,
outputDescription: o.outputDescription,
derivationSuffix: o.derivationSuffix,
customInstructions: o.customInstructions
}
outputs.push(ro)
}
return { outputs, changeVouts }
}
async function createNewTxRecord(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs,
storageBeef: Beef
): Promise<TableTransaction> {
const now = new Date()
const newTx: TableTransaction = {
created_at: now,
updated_at: now,
transactionId: 0,
version: vargs.version,
lockTime: vargs.lockTime,
status: 'unsigned',
reference: randomBytesBase64(12),
satoshis: 0, // updated after fundingTransaction
userId,
isOutgoing: true,
inputBEEF: storageBeef.toBinary(),
description: vargs.description,
txid: undefined,
rawTx: undefined
}
newTx.transactionId = await storage.insertTransaction(newTx)
for (const label of vargs.labels) {
const txLabel = await storage.findOrInsertTxLabel(userId, label)
await storage.findOrInsertTxLabelMap(verifyId(newTx.transactionId), verifyId(txLabel.txLabelId))
}
return newTx
}
/**
* Convert vargs.outputs:
*
* lockingScript: HexString
* satoshis: SatoshiValue
* outputDescription: DescriptionString5to50Bytes
* basket?: BasketStringUnder300Bytes
* customInstructions?: string
* tags: BasketStringUnderBytes[]
*
* to XValidCreateActionOutput (which aims for sdk.StorageCreateTransactionSdkOutput)
*
* adds:
* vout: number
* providedBy: sdk.StorageProvidedBy
* purpose?: string
* derivationSuffix?: string
* keyOffset?: string
*
* @param vargs
* @returns xoutputs
*/
function validateRequiredOutputs(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs
): XValidCreateActionOutput[] {
const xoutputs: XValidCreateActionOutput[] = []
let vout = -1
for (const output of vargs.outputs) {
vout++
const xo: XValidCreateActionOutput = {
...output,
vout,
providedBy: 'you',
purpose: undefined,
derivationSuffix: undefined,
keyOffset: undefined
}
xoutputs.push(xo)
}
if (storage.commissionSatoshis > 0 && storage.commissionPubKeyHex) {
vout++
const { script, keyOffset } = createStorageServiceChargeScript(storage.commissionPubKeyHex)
xoutputs.push({
lockingScript: script,
satoshis: storage.commissionSatoshis,
outputDescription: 'Storage Service Charge',
basket: undefined,
tags: [],
vout,
providedBy: 'storage',
purpose: 'service-charge',
keyOffset
})
}
return xoutputs
}
/**
* Verify that we are in posession of validity proof data for any inputs being proposed for a new transaction.
*
* `vargs.inputs` is the source of inputs.
* `vargs.inputBEEF` may include new user supplied validity data.
* 'vargs.options.trustSelf === 'known'` indicates whether we can rely on the storage database records.
*
* If there are no inputs, returns an empty `Beef`.
*
* Always pulls rawTx data into first level of validity chains so that parsed transaction data is available
* and checks input sourceSatoshis as well as filling in input sourceLockingScript.
*
* This data may be pruned again before being returned to the user based on `vargs.options.knownTxids`.
*
* @param storage
* @param userId
* @param vargs
* @returns {storageBeef} containing only validity proof data for only unknown required inputs.
* @returns {beef} containing verified validity proof data for all required inputs.
* @returns {xinputs} extended validated required inputs.
*/
async function validateRequiredInputs(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs
): Promise<{
storageBeef: Beef
beef: Beef
xinputs: XValidCreateActionInput[]
}> {
//stampLog(vargs, `start storage verifyInputBeef`)
const beef = new Beef()
if (vargs.inputs.length === 0) return { storageBeef: beef, beef, xinputs: [] }
if (vargs.inputBEEF) beef.mergeBeef(vargs.inputBEEF)
const xinputs: XValidCreateActionInput[] = vargs.inputs.map((input, vin) => ({
...input,
vin,
satoshis: -1,
lockingScript: new Script(),
output: undefined
}))
const trustSelf = vargs.options.trustSelf === 'known'
const inputTxids: Record<string, boolean> = {}
for (const input of xinputs) inputTxids[input.outpoint.txid] = true
// Check beef from user that either there are no txidOnly entries,
// or that we can trust storage data and it does indeed vouch
// for any txidOnly entries
for (const btx of beef.txs) {
if (btx.isTxidOnly) {
if (!trustSelf)
throw new sdk.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain complete proof data for ${btx.txid}`)
if (!inputTxids[btx.txid]) {
// inputTxids are checked next
const isKnown = await storage.verifyKnownValidTransaction(btx.txid)
if (!isKnown)
throw new sdk.WERR_INVALID_PARAMETER(
'inputBEEF',
`valid and contain complete proof data for unknown ${btx.txid}`
)
}
}
}
// Make sure that there's an entry for all inputs txid values:
for (const txid of Object.keys(inputTxids)) {
let btx = beef.findTxid(txid)
if (!btx && trustSelf) {
if (await storage.verifyKnownValidTransaction(txid)) btx = beef.mergeTxidOnly(txid)
}
if (!btx)
throw new sdk.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for possibly known ${txid}`)
}
if (!(await beef.verify(await storage.getServices().getChainTracker(), true))) {
console.log(`verifyInputBeef failed, inputBEEF failed to verify.\n${beef.toLogString()}\n`)
//console.log(`verifyInputBeef failed, inputBEEF failed to verify.\n${stampLogFormat(vargs.log)}\n${beef.toLogString()}\n`)
throw new sdk.WERR_INVALID_PARAMETER('inputBEEF', 'valid Beef when factoring options.trustSelf')
}
// beef may now be trusted and has a BeefTx for every input txid.
const storageBeef = beef.clone()
for (const input of xinputs) {
const { txid, vout } = input.outpoint
const output = verifyOneOrNone(await storage.findOutputs({ partial: { userId, txid, vout } }))
if (output) {
if (output.change) {
throw new sdk.WERR_INVALID_PARAMETER(
`inputs[${input.vin}]`,
'an unmanaged input. Change outputs are managed by your wallet.'
)
}
input.output = output
if (!Array.isArray(output.lockingScript) || !Number.isInteger(output.satoshis))
throw new sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'output with valid lockingScript and satoshis')
if (!disableDoubleSpendCheckForTest && !output.spendable && !vargs.isNoSend)
throw new sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'spendable output unless noSend is true')
// input is spending an existing user output which has an lockingScript
input.satoshis = verifyNumber(output.satoshis)
input.lockingScript = Script.fromBinary(asArray(output.lockingScript!))
} else {
let btx = beef.findTxid(txid)!
if (btx.isTxidOnly) {
const { rawTx, proven } = await storage.getProvenOrRawTx(txid)
//stampLog(vargs, `... storage verifyInputBeef getProvenOrRawTx ${txid} ${proven ? 'proven' : rawTx ? 'rawTx' : 'unknown'}`)
if (!rawTx) throw new sdk.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for ${txid}`)
btx = beef.mergeRawTx(asArray(rawTx))
if (proven) beef.mergeBump(new EntityProvenTx(proven).getMerklePath())
}
// btx is valid has parsed transaction data.
if (vout >= btx.tx!.outputs.length) throw new sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'valid outpoint')
const so = btx.tx!.outputs[vout]
input.satoshis = verifyTruthy(so.satoshis)
input.lockingScript = so.lockingScript
}
}
return { beef, storageBeef, xinputs }
}
async function validateNoSendChange(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs,
changeBasket: TableOutputBasket
): Promise<TableOutput[]> {
const r: TableOutput[] = []
if (!vargs.isNoSend) return []
const noSendChange = vargs.options.noSendChange
if (noSendChange && noSendChange.length > 0) {
for (const op of noSendChange) {
const output = verifyOneOrNone(
await storage.findOutputs({
partial: { userId, txid: op.txid, vout: op.vout }
})
)
// noSendChange is not marked spendable until sent, may not already be spent, and must have a valid greater than zero satoshis
if (
!output ||
output.providedBy !== 'storage' ||
output.purpose !== 'change' ||
output.spendable === false ||
Number.isInteger(output.spentBy) ||
!verifyNumber(output.satoshis) ||
output.basketId !== changeBasket.basketId
)
throw new sdk.WERR_INVALID_PARAMETER('noSendChange outpoint', 'valid')
if (-1 < r.findIndex(o => o.outputId === output.outputId))
// noSendChange duplicate OutPoints are not allowed.
throw new sdk.WERR_INVALID_PARAMETER('noSendChange outpoint', 'unique. Duplicates are not allowed.')
r.push(output)
}
}
return r
}
async function fundNewTransactionSdk(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs,
ctx: CreateTransactionSdkContext
): Promise<{
allocatedChange: TableOutput[]
changeOutputs: TableOutput[]
derivationPrefix: string
maxPossibleSatoshisAdjustment?: {
fixedOutputIndex: number
satoshis: number
}
}> {
const params: GenerateChangeSdkParams = {
fixedInputs: ctx.xinputs.map(xi => ({
satoshis: xi.satoshis,
unlockingScriptLength: xi.unlockingScriptLength!
})),
fixedOutputs: ctx.xoutputs.map(xo => ({
satoshis: xo.satoshis,
lockingScriptLength: xo.lockingScript.length / 2
})),
feeModel: ctx.feeModel,
changeInitialSatoshis: ctx.changeBasket.minimumDesiredUTXOValue,
changeFirstSatoshis: Math.max(1, Math.round(ctx.changeBasket.minimumDesiredUTXOValue / 4)),
changeLockingScriptLength: 25,
changeUnlockingScriptLength: 107,
targetNetCount: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount,
randomVals: vargs.randomVals
}
const noSendChange = [...ctx.noSendChangeIn]
const outputs: Record<number, TableOutput> = {}
const allocateChangeInput = async (
targetSatoshis: number,
exactSatoshis?: number
): Promise<GenerateChangeSdkChangeInput | undefined> => {
// noSendChange gets allocated first...typically only one input...just allocate in order...
if (noSendChange.length > 0) {
const o = noSendChange.pop()!
outputs[o.outputId!] = o
// allocate the output in storage, noSendChange is by definition spendable false and part of noSpend transaction batch.
await storage.updateOutput(o.outputId!, {
spendable: false,
spentBy: ctx.transactionId
})
o.spendable = false
o.spentBy = ctx.transactionId
const r: GenerateChangeSdkChangeInput = {
outputId: o.outputId!,
satoshis: o.satoshis!
}
return r
}
const basketId = ctx.changeBasket.basketId!
const o = await storage.allocateChangeInput(
userId,
basketId,
targetSatoshis,
exactSatoshis,
!vargs.isDelayed,
ctx.transactionId
)
if (!o) return undefined
outputs[o.outputId!] = o
const r: GenerateChangeSdkChangeInput = {
outputId: o.outputId!,
satoshis: o.satoshis!
}
return r
}
const releaseChangeInput = async (outputId: number): Promise<void> => {
const nsco = ctx.noSendChangeIn.find(o => o.outputId === outputId)
if (nsco) {
noSendChange.push(nsco)
return
}
await storage.updateOutput(outputId, {
spendable: true,
spentBy: undefined
})
}
const gcr = await generateChangeSdk(params, allocateChangeInput, releaseChangeInput)
const nextRandomVal = (): number => {
let val = 0
if (!vargs.randomVals || vargs.randomVals.length === 0) {
val = Math.random()
} else {
val = vargs.randomVals.shift() || 0
vargs.randomVals.push(val)
}
return val
}
/**
* @returns a random integer betweenn min and max, inclussive.
*/
const rand = (min: number, max: number): number => {
if (max < min) throw new sdk.WERR_INVALID_PARAMETER('max', `less than min (${min}). max is (${max})`)
return Math.floor(nextRandomVal() * (max - min + 1) + min)
}
const randomDerivation = (count: number): string => {
let val: number[] = []
if (!vargs.randomVals || vargs.randomVals.length === 0) {
val = Random(count)
} else {
for (let i = 0; i < count; i++) val.push(rand(0, 255))
}
return Utils.toBase64(val)
}
// Generate a derivation prefix for the payment
const derivationPrefix = randomDerivation(16)
const r: {
allocatedChange: TableOutput[]
changeOutputs: TableOutput[]
derivationPrefix: string
maxPossibleSatoshisAdjustment?: {
fixedOutputIndex: number
satoshis: number
}
} = {
maxPossibleSatoshisAdjustment: gcr.maxPossibleSatoshisAdjustment,
allocatedChange: gcr.allocatedChangeInputs.map(i => outputs[i.outputId]),
changeOutputs: gcr.changeOutputs.map(
(o, i) =>
<TableOutput>{
// what we knnow now and can insert into the database for this new transaction's change output
created_at: new Date(),
updated_at: new Date(),
outputId: 0,
userId,
transactionId: ctx.transactionId,
vout: params.fixedOutputs.length + i,
satoshis: o.satoshis,
basketId: ctx.changeBasket.basketId!,
spendable: false,
change: true,
type: 'P2PKH',
derivationPrefix,
derivationSuffix: randomDerivation(16),
providedBy: 'storage',
purpose: 'change',
customInstructions: undefined,
senderIdentityKey: undefined,
outputDescription: '',
// what will be known when transaction is signed
txid: undefined,
lockingScript: undefined,
// when this output gets spent
spentBy: undefined,
spendingDescription: undefined
}
),
derivationPrefix
}
return r
}
/**
* Avoid returning any known raw transaction data by converting any known transaction
* in the `beef` to txidOnly.
* @returns undefined if `vargs.options.returnTXIDOnly` or trimmed `Beef`
*/
function trimInputBeef(beef: Beef, vargs: sdk.ValidCreateActionArgs): number[] | undefined {
if (vargs.options.returnTXIDOnly) return undefined
const knownTxids: Record<string, boolean> = {}
for (const txid of vargs.options.knownTxids) knownTxids[txid] = true
for (const txid of beef.txs.map(btx => btx.txid)) if (knownTxids[txid]) beef.makeTxidOnly(txid)
return beef.toBinary()
}
async function mergeAllocatedChangeBeefs(
storage: StorageProvider,
userId: number,
vargs: sdk.ValidCreateActionArgs,
allocatedChange: TableOutput[],
beef: Beef
): Promise<number[] | undefined> {
const options: sdk.StorageGetBeefOptions = {
trustSelf: undefined,
knownTxids: vargs.options.knownTxids,
mergeToBeef: beef,
ignoreStorage: false,
ignoreServices: true,
ignoreNewProven: false,
minProofLevel: undefined
}
if (vargs.options.returnTXIDOnly) return undefined
for (const o of allocatedChange) {
if (!beef.findTxid(o.txid!) && !vargs.options.knownTxids.find(txid => txid === o.txid)) {
await storage.getBeefForTransaction(o.txid!, options)
}
}
return trimInputBeef(beef, vargs)
}
function keyOffsetToHashedSecret(pub: PublicKey, keyOffset?: string): { hashedSecret: BigNumber; keyOffset: string } {
let offset: PrivateKey
if (keyOffset !== undefined && typeof keyOffset === 'string') {
if (keyOffset.length === 64) offset = PrivateKey.fromString(keyOffset, 'hex')
else offset = PrivateKey.fromWif(keyOffset)
} else {
offset = PrivateKey.fromRandom()
keyOffset = offset.toWif()
}
const sharedSecret = pub.mul(offset).encode(true, undefined) as number[]
const hashedSecret = sha256Hash(sharedSecret)
return { hashedSecret: new BigNumber(hashedSecret), keyOffset }
}
export function offsetPubKey(pubKey: string, keyOffset?: string): { offsetPubKey: string; keyOffset: string } {
const pub = PublicKey.fromString(pubKey)
const r = keyOffsetToHashedSecret(pub, keyOffset)
// The hashed secret is multiplied by the generator point.
const point = new Curve().g.mul(r.hashedSecret)
// The resulting point is added to the recipient public key.
const offsetPubKey = new PublicKey(pub.add(point))
return { offsetPubKey: offsetPubKey.toString(), keyOffset: r.keyOffset }
}
export function lockScriptWithKeyOffsetFromPubKey(
pubKey: string,
keyOffset?: string
): { script: string; keyOffset: string } {
const r = offsetPubKey(pubKey, keyOffset)
const offsetPub = PublicKey.fromString(r.offsetPubKey)
const hash = offsetPub.toHash() as number[]
const script = new P2PKH().lock(hash).toHex()
return { script, keyOffset: r.keyOffset }
}
export function createStorageServiceChargeScript(pubKeyHex: PubKeyHex): {
script: string
keyOffset: string
} {
return lockScriptWithKeyOffsetFromPubKey(pubKeyHex)
}