wallet-storage-client
Version:
Client only Wallet Storage
227 lines • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAction = createAction;
exports.completeSignedTransaction = completeSignedTransaction;
exports.processAction = processAction;
const sdk_1 = require("@bsv/sdk");
const index_client_1 = require("../../index.client");
async function createAction(wallet, auth, vargs) {
var _a;
const r = {};
let prior = 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');
r.noSendChange = (_a = prior.dcr.noSendChangeOutputVouts) === null || _a === void 0 ? void 0 : _a.map(vout => `${r.txid}.${vout}`);
if (!vargs.options.returnTXIDOnly)
r.tx = (0, index_client_1.makeAtomicBeef)(prior.tx, prior.dcr.inputBeef);
}
r.sendWithResults = await processAction(prior, wallet, auth, vargs);
return r;
}
async function createNewTx(wallet, args) {
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 = { reference, dcr, args, amount, tx, pdi };
return prior;
}
function makeSignableTransactionResult(prior, wallet, args) {
var _a;
if (!prior.dcr.inputBeef)
throw new index_client_1.sdk.WERR_INTERNAL('prior.dcr.inputBeef must be valid');
const txid = prior.tx.id('hex');
const r = {
noSendChange: args.isNoSend ? (_a = prior.dcr.noSendChangeOutputVouts) === null || _a === void 0 ? void 0 : _a.map(vout => `${txid}.${vout}`) : undefined,
signableTransaction: {
reference: prior.dcr.reference,
tx: (0, index_client_1.makeAtomicBeef)(prior.tx, prior.dcr.inputBeef)
}
};
wallet.pendingSignActions[r.signableTransaction.reference] = prior;
return r;
}
/**
* Derive a change output locking script
*/
function makeChangeLock(out, dctr, args, changeKeys, wallet) {
const derivationPrefix = dctr.derivationPrefix;
const derivationSuffix = (0, index_client_1.verifyTruthy)(out.derivationSuffix);
const sabppp = new index_client_1.ScriptTemplateSABPPP({ derivationPrefix, derivationSuffix, keyDeriver: wallet.keyDeriver });
const lockingScript = sabppp.lock(changeKeys.privateKey, changeKeys.publicKey);
return lockingScript;
}
async function completeSignedTransaction(prior, spends, wallet) {
/////////////////////
// Insert the user provided unlocking scripts from "spends" arg
/////////////////////
for (const [key, spend] of Object.entries(spends)) {
const vin = Number(key);
const createInput = prior.args.inputs[vin];
const input = prior.tx.inputs[vin];
if (!createInput || !input || createInput.unlockingScript || !Number.isInteger(createInput.unlockingScriptLength))
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('args', `spend does not correspond to prior input with valid unlockingScriptLength.`);
if (spend.unlockingScript.length / 2 > createInput.unlockingScriptLength)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('args', `spend unlockingScript length ${spend.unlockingScript.length} exceeds expected length ${createInput.unlockingScriptLength}`);
input.unlockingScript = (0, index_client_1.asBsvSdkScript)(spend.unlockingScript);
if (spend.sequenceNumber !== undefined)
input.sequence = spend.sequenceNumber;
}
const results = {
sdk: {}
};
/////////////////////
// Insert SABPPP unlock templates for storage signed inputs
/////////////////////
for (const pdi of prior.pdi) {
const sabppp = new index_client_1.ScriptTemplateSABPPP({
derivationPrefix: pdi.derivationPrefix,
derivationSuffix: pdi.derivationSuffix,
keyDeriver: wallet.keyDeriver
});
const keys = wallet.getClientChangeKeyPair();
const lockerPrivKey = keys.privateKey;
const unlockerPubKey = pdi.unlockerPubKey || keys.publicKey;
const sourceSatoshis = pdi.sourceSatoshis;
const lockingScript = (0, index_client_1.asBsvSdkScript)(pdi.lockingScript);
const unlockTemplate = sabppp.unlock(lockerPrivKey, unlockerPubKey, sourceSatoshis, lockingScript);
const input = prior.tx.inputs[pdi.vin];
input.unlockingScriptTemplate = unlockTemplate;
}
/////////////////////
// Sign storage signed inputs making transaction fully valid.
/////////////////////
await prior.tx.sign();
return prior.tx;
}
function removeUnlockScripts(args) {
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 = {
...i,
unlockingScriptLength: i.unlockingScript !== undefined ? i.unlockingScript.length : i.unlockingScriptLength
};
delete di.unlockingScript;
storageArgs.inputs.push(di);
}
}
return storageArgs;
}
async function processAction(prior, wallet, auth, vargs) {
const args = {
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 = await wallet.storage.processAction(args);
return r.sendWithResults;
}
function buildSignableTransaction(dctr, args, wallet) {
const changeKeys = wallet.getClientChangeKeyPair();
const { inputs: storageInputs, outputs: storageOutputs, } = dctr;
const tx = new sdk_1.Transaction(args.version, [], [], args.lockTime);
// The order of outputs in storageOutputs is always:
// CreateActionArgs.outputs in the original order
// Commission output
// Change outputs
// The Vout values will be randomized if args.options.randomizeOutputs is true. Default is true.
const voutToIndex = Array(storageOutputs.length);
for (let vout = 0; vout < storageOutputs.length; vout++) {
const i = storageOutputs.findIndex(o => o.vout === vout);
if (i < 0)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('output.vout', `sequential. ${vout} is missing`);
voutToIndex[vout] = i;
}
//////////////
// Add OUTPUTS
/////////////
for (let vout = 0; vout < storageOutputs.length; vout++) {
const i = voutToIndex[vout];
const out = storageOutputs[i];
if (vout !== out.vout)
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('output.vout', `equal to array index. ${out.vout} !== ${vout}`);
const change = out.providedBy === 'storage' && out.purpose === 'change';
const lockingScript = change ? makeChangeLock(out, dctr, args, changeKeys, wallet) : (0, index_client_1.asBsvSdkScript)(out.lockingScript);
const output = {
satoshis: out.satoshis,
lockingScript,
change
};
tx.addOutput(output);
}
//////////////
// Merge and sort INPUTS info by vin order.
/////////////
const inputs = [];
for (const storageInput of storageInputs) {
const argsInput = (storageInput.vin !== undefined && storageInput.vin < args.inputs.length) ? args.inputs[storageInput.vin] : undefined;
inputs.push({ argsInput, storageInput });
}
inputs.sort((a, b) => a.storageInput.vin < b.storageInput.vin ? -1 : a.storageInput.vin === b.storageInput.vin ? 0 : 1);
const pendingStorageInputs = [];
//////////////
// Add INPUTS
/////////////
let totalChangeInputs = 0;
for (const { storageInput, argsInput } of inputs) {
// Two types of inputs are handled: user specified wth/without unlockingScript and storage specified using SABPPP template.
if (argsInput) {
// Type 1: User supplied input, with or without an explicit unlockingScript.
// If without, signAction must be used to provide the actual unlockScript.
const hasUnlock = typeof argsInput.unlockingScript === 'string';
const unlock = hasUnlock ? (0, index_client_1.asBsvSdkScript)(argsInput.unlockingScript) : new sdk_1.Script();
const inputToAdd = {
sourceTXID: argsInput.outpoint.txid,
sourceOutputIndex: argsInput.outpoint.vout,
unlockingScript: unlock,
sequence: argsInput.sequenceNumber
};
tx.addInput(inputToAdd);
}
else {
// Type2: SABPPP protocol inputs which are signed using ScriptTemplateSABPPP.
if (storageInput.type !== 'P2PKH')
throw new index_client_1.sdk.WERR_INVALID_PARAMETER('type', `vin ${storageInput.vin}, "${storageInput.type}" is not a supported unlocking script type.`);
pendingStorageInputs.push({
vin: tx.inputs.length,
derivationPrefix: (0, index_client_1.verifyTruthy)(storageInput.derivationPrefix),
derivationSuffix: (0, index_client_1.verifyTruthy)(storageInput.derivationSuffix),
unlockerPubKey: storageInput.senderIdentityKey,
sourceSatoshis: storageInput.sourceSatoshis,
lockingScript: storageInput.sourceLockingScript
});
const inputToAdd = {
sourceTXID: storageInput.sourceTxid,
sourceOutputIndex: storageInput.sourceVout,
unlockingScript: new sdk_1.Script(),
sequence: 0xffffffff
};
tx.addInput(inputToAdd);
totalChangeInputs += (0, index_client_1.verifyTruthy)(storageInput.sourceSatoshis);
}
}
// The amount is the total of non-foreign inputs minus change outputs
// Note that the amount can be negative when we are redeeming more inputs than we are spending
const totalChangeOutputs = storageOutputs.filter(x => x.purpose === 'change').reduce((acc, el) => acc + el.satoshis, 0);
const amount = totalChangeInputs - totalChangeOutputs;
return {
tx,
amount,
pdi: pendingStorageInputs,
log: ''
};
}
//# sourceMappingURL=createAction.js.map