wallet-storage-client
Version:
Client only Wallet Storage
635 lines • 27.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAction = createAction;
exports.offsetPubKey = offsetPubKey;
exports.lockScriptWithKeyOffsetFromPubKey = lockScriptWithKeyOffsetFromPubKey;
exports.createStorageServiceChargeScript = createStorageServiceChargeScript;
const sdk_1 = require("@bsv/sdk");
const index_client_1 = require("../../index.client");
const generateChange_1 = require("./generateChange");
async function createAction(storage, auth, vargs, originator) {
//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 index_client_1.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 = (0, index_client_1.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 = (0, index_client_1.validateStorageFeeModel)(storage.feeModel);
const newTx = await createNewTxRecord(storage, userId, vargs, storageBeef);
const ctx = { xinputs, xoutputs, changeBasket, noSendChangeIn, availableChangeCount, feeModel, transactionId: newTx.transactionId };
const { allocatedChange, changeOutputs, derivationPrefix } = await fundNewTransactionSdk(storage, userId, vargs, ctx);
// 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 = {
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;
}
function makeDefaultOutput(userId, transactionId, satoshis, vout) {
const now = new Date();
const output = {
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, userId, vargs, ctx, allocatedChange) {
const r = [];
const newInputs = [];
for (const i of ctx.xinputs) {
const o = i.output;
newInputs.push({ i, o });
if (o) {
await storage.updateOutput(o.outputId, {
spendable: false,
spentBy: ctx.transactionId,
spendingDescription: i.inputDescription
});
}
}
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 index_client_1.sdk.WERR_INTERNAL(`vin ${vin} non-fixedInput without unlockLen`);
const ri = {
vin,
sourceTxid: o.txid,
sourceVout: o.vout,
sourceSatoshis: o.satoshis,
sourceLockingScript: (0, index_client_1.asString)(o.lockingScript),
unlockingScriptLength: unlockLen ? unlockLen : i.unlockingScriptLength,
providedBy: i && o.providedBy === 'storage' ? 'you-and-storage' : o.providedBy,
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 index_client_1.sdk.WERR_INTERNAL(`vin ${vin} without output or xinput`);
// user specified input with no corresponding output being spent.
const ri = {
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, userId, vargs, ctx, changeOutputs) {
var _a;
const outputs = [];
// Lookup output baskets
const txBaskets = {};
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 = {};
for (const xo of ctx.xoutputs) {
for (const tag of xo.tags) {
txTags[tag] = await storage.findOrInsertOutputTag(userId, tag);
}
}
const newOutputs = [];
for (const xo of ctx.xoutputs) {
const lockingScript = (0, index_client_1.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: (0, index_client_1.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 = [];
const nextRandomVal = () => {
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 = (array) => {
let currentIndex = array.length;
let temporaryValue;
let randomIndex;
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(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 index_client_1.sdk.WERR_INTERNAL(`new output ${vout} has out of order vout ${no.o.vout}`);
no.o.vout = newVouts[vout];
}
}
const changeVouts = [];
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((0, index_client_1.verifyId)(o.outputId), (0, index_client_1.verifyId)(tag.outputTagId));
}
const ro = {
vout: (0, index_client_1.verifyTruthy)(o.vout),
satoshis: (0, index_client_1.verifyTruthy)(o.satoshis),
lockingScript: !o.lockingScript ? '' : (0, index_client_1.asString)(o.lockingScript),
providedBy: (0, index_client_1.verifyTruthy)(o.providedBy),
purpose: o.purpose || undefined,
basket: (_a = Object.values(txBaskets).find(b => b.basketId === o.basketId)) === null || _a === void 0 ? void 0 : _a.name,
tags: tags,
outputDescription: o.outputDescription,
derivationSuffix: o.derivationSuffix,
customInstructions: o.customInstructions
};
outputs.push(ro);
}
return { outputs, changeVouts };
}
async function createNewTxRecord(storage, userId, vargs, storageBeef) {
const now = new Date();
const newTx = {
created_at: now,
updated_at: now,
transactionId: 0,
version: vargs.version,
lockTime: vargs.lockTime,
status: 'unsigned',
reference: (0, index_client_1.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((0, index_client_1.verifyId)(newTx.transactionId), (0, index_client_1.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, userId, vargs) {
const xoutputs = [];
let vout = -1;
for (const output of vargs.outputs) {
vout++;
const xo = {
...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, userId, vargs) {
//stampLog(vargs, `start storage verifyInputBeef`)
const beef = new sdk_1.Beef();
if (vargs.inputs.length === 0)
return { storageBeef: beef, beef, xinputs: [] };
if (vargs.inputBEEF)
beef.mergeBeef(vargs.inputBEEF);
const xinputs = vargs.inputs.map((input, vin) => ({ ...input, vin, satoshis: -1, lockingScript: new sdk_1.Script() }));
const trustSelf = vargs.options.trustSelf === 'known';
const inputTxids = {};
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 index_client_1.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 index_client_1.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 index_client_1.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 index_client_1.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 = (0, index_client_1.verifyOneOrNone)(await storage.findOutputs({ partial: { userId, txid, vout } }));
if (output) {
input.output = output;
if (!Array.isArray(output.lockingScript) || !Number.isInteger(output.satoshis))
throw new index_client_1.sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'output with valid lockingScript and satoshis');
if (!output.spendable && !vargs.isNoSend)
throw new index_client_1.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 = (0, index_client_1.verifyNumber)(output.satoshis);
input.lockingScript = sdk_1.Script.fromBinary((0, index_client_1.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 index_client_1.sdk.WERR_INVALID_PARAMETER('inputBEEF', `valid and contain proof data for ${txid}`);
btx = beef.mergeRawTx((0, index_client_1.asArray)(rawTx));
if (proven)
beef.mergeBump(new index_client_1.entity.ProvenTx(proven).getMerklePath());
}
// btx is valid has parsed transaction data.
if (vout >= btx.tx.outputs.length)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER(`${txid}.${vout}`, 'valid outpoint');
const so = btx.tx.outputs[vout];
input.satoshis = (0, index_client_1.verifyTruthy)(so.satoshis);
input.lockingScript = so.lockingScript;
}
}
return { beef, storageBeef, xinputs };
}
async function validateNoSendChange(dojo, userId, vargs, changeBasket) {
const r = [];
if (!vargs.isNoSend)
return [];
const noSendChange = vargs.options.noSendChange;
if (noSendChange && noSendChange.length > 0) {
for (const op of noSendChange) {
const output = (0, index_client_1.verifyOneOrNone)(await dojo.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)
|| !(0, index_client_1.verifyNumber)(output.satoshis)
|| output.basketId !== changeBasket.basketId)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('noSendChange outpoint', 'valid');
if (-1 < r.findIndex(o => o.outputId === output.outputId))
// noSendChange duplicate OutPoints are not allowed.
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('noSendChange outpoint', 'unique. Duplicates are not allowed.');
r.push(output);
}
}
return r;
}
async function fundNewTransactionSdk(dojo, userId, vargs, ctx) {
const params = {
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, ctx.changeBasket.minimumDesiredUTXOValue / 4),
changeLockingScriptLength: 25,
changeUnlockingScriptLength: 107,
targetNetCount: ctx.changeBasket.numberOfDesiredUTXOs - ctx.availableChangeCount
};
const noSendChange = [...ctx.noSendChangeIn];
const outputs = {};
const allocateChangeInput = async (targetSatoshis, exactSatoshis) => {
// 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 dojo.updateOutput(o.outputId, {
spendable: false,
spentBy: ctx.transactionId
});
o.spendable = false;
o.spentBy = ctx.transactionId;
const r = {
outputId: o.outputId,
satoshis: o.satoshis
};
return r;
}
const basketId = ctx.changeBasket.basketId;
const o = await dojo.allocateChangeInput(userId, basketId, targetSatoshis, exactSatoshis, !vargs.isDelayed, ctx.transactionId);
if (!o)
return undefined;
outputs[o.outputId] = o;
const r = {
outputId: o.outputId,
satoshis: o.satoshis
};
return r;
};
const releaseChangeInput = async (outputId) => {
const nsco = ctx.noSendChangeIn.find(o => o.outputId === outputId);
if (nsco) {
noSendChange.push(nsco);
return;
}
await dojo.updateOutput(outputId, {
spendable: true,
spentBy: undefined
});
};
const gcr = await (0, generateChange_1.generateChangeSdk)(params, allocateChangeInput, releaseChangeInput);
// Generate a derivation prefix for the payment
const derivationPrefix = (0, index_client_1.randomBytesBase64)(16);
const r = {
allocatedChange: gcr.allocatedChangeInputs.map(i => outputs[i.outputId]),
changeOutputs: gcr.changeOutputs.map((o, i) => ({
// 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: (0, index_client_1.randomBytesBase64)(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, vargs) {
if (vargs.options.returnTXIDOnly)
return undefined;
const knownTxids = {};
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(dojo, userId, vargs, allocatedChange, beef) {
const options = {
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 dojo.getBeefForTransaction(o.txid, options);
}
}
return trimInputBeef(beef, vargs);
}
function keyOffsetToHashedSecret(pub, keyOffset) {
let offset;
if (keyOffset !== undefined && typeof keyOffset === 'string') {
if (keyOffset.length === 64)
offset = sdk_1.PrivateKey.fromString(keyOffset, 'hex');
else
offset = sdk_1.PrivateKey.fromWif(keyOffset);
}
else {
offset = sdk_1.PrivateKey.fromRandom();
keyOffset = offset.toWif();
}
const sharedSecret = pub.mul(offset).encode(true, undefined);
const hashedSecret = (0, index_client_1.sha256Hash)(sharedSecret);
return { hashedSecret: new sdk_1.BigNumber(hashedSecret), keyOffset };
}
function offsetPubKey(pubKey, keyOffset) {
const pub = sdk_1.PublicKey.fromString(pubKey);
const r = keyOffsetToHashedSecret(pub, keyOffset);
// The hashed secret is multiplied by the generator point.
const point = new sdk_1.Curve().g.mul(r.hashedSecret);
// The resulting point is added to the recipient public key.
const offsetPubKey = new sdk_1.PublicKey(pub.add(point));
return { offsetPubKey: offsetPubKey.toString(), keyOffset: r.keyOffset };
}
function lockScriptWithKeyOffsetFromPubKey(pubKey, keyOffset) {
const r = offsetPubKey(pubKey, keyOffset);
const offsetPub = sdk_1.PublicKey.fromString(r.offsetPubKey);
const hash = offsetPub.toHash();
const script = new sdk_1.P2PKH().lock(hash).toHex();
return { script, keyOffset: r.keyOffset };
}
function createStorageServiceChargeScript(pubKeyHex) {
return lockScriptWithKeyOffsetFromPubKey(pubKeyHex);
}
//# sourceMappingURL=createAction.js.map