@bsv/sdk
Version:
BSV Blockchain Software Development Kit
225 lines • 10.1 kB
JavaScript
import { LockingScript, UnlockingScript, OP } from '../index.js';
import { Utils, Hash, TransactionSignature, Signature, PublicKey } from '../../primitives/index.js';
function verifyTruthy(v) {
if (v == null)
throw new Error('must have value');
return v;
}
/**
* For a given piece of data to push onto the stack in script, creates the correct minimally-encoded script chunk,
* including the correct push operation.
*
* TODO: This should be made into a TS-SDK util (distinct from the `minimallyEncode` util)
*/
const createMinimallyEncodedScriptChunk = (data) => {
if (data.length === 0) {
// Could have used OP_0.
return { op: 0 };
}
if (data.length === 1 && data[0] === 0) {
// Could have used OP_0.
return { op: 0 };
}
if (data.length === 1 && data[0] > 0 && data[0] <= 16) {
// Could have used OP_0 .. OP_16.
return { op: 0x50 + data[0] };
}
if (data.length === 1 && data[0] === 0x81) {
// Could have used OP_1NEGATE.
return { op: 0x4f };
}
if (data.length <= 75) {
// Could have used a direct push (opcode indicating number of bytes
// pushed + those bytes).
return { op: data.length, data };
}
if (data.length <= 255) {
// Could have used OP_PUSHDATA.
return { op: 0x4c, data };
}
if (data.length <= 65535) {
// Could have used OP_PUSHDATA2.
return { op: 0x4d, data };
}
return { op: 0x4e, data };
};
export default class PushDrop {
wallet;
originator;
/**
* Decodes a PushDrop script back into its token fields and the locking public key. If a signature was present, it will be the last field returned.
* Warning: Only works with a P2PK lock at the beginning of the script.
* @param script PushDrop script to decode back into token fields
* @returns An object containing PushDrop token fields and the locking public key. If a signature was included, it will be the last field.
*/
static decode(script) {
const lockingPublicKey = PublicKey.fromString(Utils.toHex(verifyTruthy(script.chunks[0].data)) // ✅ Ensure not undefined
);
const fields = [];
for (let i = 2; i < script.chunks.length; i++) {
const nextOpcode = script.chunks[i + 1]?.op; // ✅ Prevent accessing `op` from `undefined`
let chunk = script.chunks[i].data ?? []; // ✅ Ensure `chunk` is always `number[]`
if (chunk.length === 0) {
// ✅ Only modify `chunk` if it was empty
if (script.chunks[i].op >= 80 && script.chunks[i].op <= 95) {
chunk = [script.chunks[i].op - 80];
}
else if (script.chunks[i].op === 0) {
chunk = [0];
}
else if (script.chunks[i].op === 0x4f) {
chunk = [0x81];
}
}
fields.push(chunk);
// If the next value is DROP or 2DROP then this is the final field
if (nextOpcode === OP.OP_DROP || nextOpcode === OP.OP_2DROP) {
break;
}
}
return {
fields,
lockingPublicKey
};
}
/**
* Constructs a new instance of the PushDrop class.
*
* @param {WalletInterface} wallet - The wallet interface used for creating signatures and accessing public keys.
* @param {string} originator — The originator to use with Wallet requests
*/
constructor(wallet, originator) {
this.wallet = wallet;
this.originator = originator;
}
/**
* Creates a PushDrop locking script with arbitrary data fields and a public key lock.
*
* @param {number[][]} fields - The token fields to include in the locking script.
* @param {WalletProtocol} protocolID - The protocol ID to use.
* @param {string} keyID - The key ID to use.
* @param {string} counterparty - The counterparty involved in the transaction, "self" or "anyone".
* @param {boolean} [forSelf=false] - Flag indicating if the lock is for the creator (default no).
* @param {boolean} [includeSignature=true] - Flag indicating if a signature should be included in the script (default yes).
* @returns {Promise<LockingScript>} The generated PushDrop locking script.
*/
async lock(fields, protocolID, keyID, counterparty, forSelf = false, includeSignature = true, lockPosition = 'before') {
const { publicKey } = await this.wallet.getPublicKey({
protocolID,
keyID,
counterparty,
forSelf
}, this.originator);
const lockChunks = [];
const pushDropChunks = [];
lockChunks.push({
op: publicKey.length / 2,
data: Utils.toArray(publicKey, 'hex')
});
lockChunks.push({ op: OP.OP_CHECKSIG });
if (includeSignature) {
const dataToSign = fields.reduce((a, e) => [...a, ...e], []);
const { signature } = await this.wallet.createSignature({
data: dataToSign,
protocolID,
keyID,
counterparty
}, this.originator);
fields.push(signature);
}
for (const field of fields) {
pushDropChunks.push(createMinimallyEncodedScriptChunk(field));
}
let notYetDropped = fields.length;
while (notYetDropped > 1) {
pushDropChunks.push({ op: OP.OP_2DROP });
notYetDropped -= 2;
}
if (notYetDropped !== 0) {
pushDropChunks.push({ op: OP.OP_DROP });
}
if (lockPosition === 'before') {
return new LockingScript([...lockChunks, ...pushDropChunks]);
}
else {
return new LockingScript([...pushDropChunks, ...lockChunks]);
}
}
/**
* Creates an unlocking script for spending a PushDrop token output.
*
* @param {WalletProtocol} protocolID - The protocol ID to use.
* @param {string} keyID - The key ID to use.
* @param {string} counterparty - The counterparty involved in the transaction, "self" or "anyone".
* @param {string} [sourceTXID] - The TXID of the source transaction.
* @param {number} [sourceSatoshis] - The number of satoshis in the source output.
* @param {LockingScript} [lockingScript] - The locking script of the source output.
* @param {'all' | 'none' | 'single'} [signOutputs='all'] - Specifies which outputs to sign.
* @param {boolean} [anyoneCanPay=false] - Specifies if the anyone-can-pay flag is set.
* @returns {Object} An object containing functions to sign the transaction and estimate the script length.
*/
unlock(protocolID, keyID, counterparty, signOutputs = 'all', anyoneCanPay = false, sourceSatoshis, lockingScript) {
return {
sign: async (tx, inputIndex) => {
let signatureScope = TransactionSignature.SIGHASH_FORKID;
if (signOutputs === 'all') {
signatureScope |= TransactionSignature.SIGHASH_ALL;
}
if (signOutputs === 'none') {
signatureScope |= TransactionSignature.SIGHASH_NONE;
}
if (signOutputs === 'single') {
signatureScope |= TransactionSignature.SIGHASH_SINGLE;
}
if (anyoneCanPay) {
signatureScope |= TransactionSignature.SIGHASH_ANYONECANPAY;
}
const input = tx.inputs[inputIndex];
const otherInputs = tx.inputs.filter((_, index) => index !== inputIndex);
const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id('hex');
if (sourceTXID == null || sourceTXID === undefined) {
throw new Error('The input sourceTXID or sourceTransaction is required for transaction signing.');
}
sourceSatoshis ||=
input.sourceTransaction?.outputs[input.sourceOutputIndex].satoshis;
if (sourceSatoshis == null || sourceSatoshis === undefined) {
throw new Error('The sourceSatoshis or input sourceTransaction is required for transaction signing.');
}
lockingScript ||=
input.sourceTransaction?.outputs[input.sourceOutputIndex]
.lockingScript;
if (lockingScript == null) {
throw new Error('The lockingScript or input sourceTransaction is required for transaction signing.');
}
const preimage = TransactionSignature.format({
sourceTXID,
sourceOutputIndex: verifyTruthy(input.sourceOutputIndex),
sourceSatoshis,
transactionVersion: tx.version,
otherInputs,
inputIndex,
outputs: tx.outputs,
inputSequence: input.sequence ?? 0xffffffff,
subscript: lockingScript,
lockTime: tx.lockTime,
scope: signatureScope
});
const preimageHash = Hash.sha256(preimage);
const { signature: bareSignature } = await this.wallet.createSignature({
data: preimageHash,
protocolID,
keyID,
counterparty
}, this.originator);
const signature = Signature.fromDER([...bareSignature]);
const txSignature = new TransactionSignature(signature.r, signature.s, signatureScope);
const sigForScript = txSignature.toChecksigFormat();
return new UnlockingScript([
{ op: sigForScript.length, data: sigForScript }
]);
},
estimateLength: async () => 73
};
}
}
//# sourceMappingURL=PushDrop.js.map