|
@ -1,11 +1,24 @@ |
|
|
import { Psbt, Transaction } from 'bitcoinjs-lib'; |
|
|
import { Psbt, Transaction } from 'bitcoinjs-lib'; |
|
|
import { p2wpkh } from 'bitcoinjs-lib/types/payments'; |
|
|
import { payments } from 'bitcoinjs-lib'; |
|
|
import { Bip32Derivation, GlobalXpub, PsbtInput } from 'bip174/src/lib/interfaces'; |
|
|
import { |
|
|
import { Input } from 'bitcoinjs-lib/types/transaction'; |
|
|
Bip32Derivation, |
|
|
|
|
|
GlobalXpub, |
|
|
|
|
|
PsbtInput, |
|
|
|
|
|
} from 'bip174/src/lib/interfaces'; |
|
|
|
|
|
|
|
|
type Nullable<T> = T | null; |
|
|
type Nullable<T> = T | null; |
|
|
|
|
|
interface TxInput { |
|
|
|
|
|
hash: Buffer; |
|
|
|
|
|
index: number; |
|
|
|
|
|
script: Buffer; |
|
|
|
|
|
sequence: number; |
|
|
|
|
|
witness: Buffer[]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export async function requestPayjoinWithCustomRemoteCall(psbt: Psbt, remoteCall: (psbt: Psbt) => Promise<Nullable<Psbt>>) { |
|
|
export async function requestPayjoinWithCustomRemoteCall( |
|
|
|
|
|
psbt: Psbt, |
|
|
|
|
|
remoteCall: (psbt: Psbt) => Promise<Nullable<Psbt>>, |
|
|
|
|
|
): Promise<void> { |
|
|
const clonedPsbt = psbt.clone(); |
|
|
const clonedPsbt = psbt.clone(); |
|
|
clonedPsbt.finalizeAllInputs(); |
|
|
clonedPsbt.finalizeAllInputs(); |
|
|
|
|
|
|
|
@ -13,29 +26,43 @@ export async function requestPayjoinWithCustomRemoteCall(psbt: Psbt, remoteCall: |
|
|
for (let index = 0; index < clonedPsbt.inputCount; index++) { |
|
|
for (let index = 0; index < clonedPsbt.inputCount; index++) { |
|
|
clonedPsbt.clearFinalizedInput(index); |
|
|
clonedPsbt.clearFinalizedInput(index); |
|
|
} |
|
|
} |
|
|
clonedPsbt.data.outputs.forEach(output => { |
|
|
clonedPsbt.data.outputs.forEach((output): void => { |
|
|
delete output.bip32Derivation; |
|
|
delete output.bip32Derivation; |
|
|
}); |
|
|
}); |
|
|
delete clonedPsbt.data.globalMap.globalXpub; |
|
|
delete clonedPsbt.data.globalMap.globalXpub; |
|
|
|
|
|
|
|
|
const payjoinPsbt = await remoteCall(clonedPsbt); |
|
|
const payjoinPsbt = await remoteCall(clonedPsbt); |
|
|
if (!payjoinPsbt) throw new Error('We did not get the receiver\'s PSBT'); |
|
|
if (!payjoinPsbt) throw new Error("We did not get the receiver's PSBT"); |
|
|
|
|
|
|
|
|
// no inputs were added?
|
|
|
// no inputs were added?
|
|
|
if (clonedPsbt.inputCount <= payjoinPsbt.inputCount) { |
|
|
if (clonedPsbt.inputCount <= payjoinPsbt.inputCount) { |
|
|
throw new Error('There were less inputs than before in the receiver\'s PSBT'); |
|
|
throw new Error( |
|
|
|
|
|
"There were less inputs than before in the receiver's PSBT", |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (payjoinPsbt.data.globalMap.globalXpub && (payjoinPsbt.data.globalMap.globalXpub as GlobalXpub[]).length > 0) { |
|
|
if ( |
|
|
throw new Error('GlobalXPubs should not be included in the receiver\'s PSBT'); |
|
|
payjoinPsbt.data.globalMap.globalXpub && |
|
|
|
|
|
(payjoinPsbt.data.globalMap.globalXpub as GlobalXpub[]).length > 0 |
|
|
|
|
|
) { |
|
|
|
|
|
throw new Error( |
|
|
|
|
|
"GlobalXPubs should not be included in the receiver's PSBT", |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
if (hasKeypathInformationSet(payjoinPsbt.data.outputs) || hasKeypathInformationSet(payjoinPsbt.data.inputs)) { |
|
|
if ( |
|
|
throw new Error(('Keypath information should not be included in the receiver\'s PSBT'); |
|
|
hasKeypathInformationSet(payjoinPsbt.data.outputs) || |
|
|
|
|
|
hasKeypathInformationSet(payjoinPsbt.data.inputs) |
|
|
|
|
|
) { |
|
|
|
|
|
throw new Error( |
|
|
|
|
|
"Keypath information should not be included in the receiver's PSBT", |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const sanityResult = checkSanity(payjoinPsbt); |
|
|
const sanityResult = checkSanity(payjoinPsbt); |
|
|
if(Object.keys(sanityResult).length > 0){ |
|
|
if (Object.keys(sanityResult).length > 0) { |
|
|
throw new Error(`Receiver's PSBT is insane: ${JSON.stringify(sanityResult)}`); |
|
|
throw new Error( |
|
|
|
|
|
`Receiver's PSBT is insane: ${JSON.stringify(sanityResult)}`, |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// We make sure we don't sign what should not be signed
|
|
|
// We make sure we don't sign what should not be signed
|
|
@ -50,7 +77,7 @@ export async function requestPayjoinWithCustomRemoteCall(psbt: Psbt, remoteCall: |
|
|
const outputLegacy = getGlobalTransaction(payjoinPsbt).outs[index]; |
|
|
const outputLegacy = getGlobalTransaction(payjoinPsbt).outs[index]; |
|
|
// Make sure only our output has any information
|
|
|
// Make sure only our output has any information
|
|
|
delete output.bip32Derivation; |
|
|
delete output.bip32Derivation; |
|
|
psbt.data.outputs.forEach(originalOutput => { |
|
|
psbt.data.outputs.forEach((originalOutput): void => { |
|
|
// update the payjoin outputs
|
|
|
// update the payjoin outputs
|
|
|
if ( |
|
|
if ( |
|
|
outputLegacy.script.equals( |
|
|
outputLegacy.script.equals( |
|
@ -58,9 +85,9 @@ export async function requestPayjoinWithCustomRemoteCall(psbt: Psbt, remoteCall: |
|
|
// Can we assume output will contain redeemScript and witnessScript?
|
|
|
// Can we assume output will contain redeemScript and witnessScript?
|
|
|
// If so, we could decompile scriptPubkey, RS, and WS, and search for
|
|
|
// If so, we could decompile scriptPubkey, RS, and WS, and search for
|
|
|
// the pubkey and its hash160.
|
|
|
// the pubkey and its hash160.
|
|
|
p2wpkh({ |
|
|
payments.p2wpkh({ |
|
|
pubkey: originalOutput.bip32Derivation.pubkey, |
|
|
pubkey: originalOutput.bip32Derivation![0].pubkey, |
|
|
}).output, |
|
|
}).output!, |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
payjoinPsbt.updateOutput(index, originalOutput); |
|
|
payjoinPsbt.updateOutput(index, originalOutput); |
|
@ -79,14 +106,23 @@ export async function requestPayjoinWithCustomRemoteCall(psbt: Psbt, remoteCall: |
|
|
// TODO: * check if the difference is due to adjusting fee to increase transaction size
|
|
|
// TODO: * check if the difference is due to adjusting fee to increase transaction size
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export function requestPayjoin(psbt: Psbt, payjoinEndpoint: string) { |
|
|
export async function requestPayjoin( |
|
|
return requestPayjoinWithCustomRemoteCall(psbt, psbt1 => doRequest(psbt1, payjoinEndpoint)); |
|
|
psbt: Psbt, |
|
|
|
|
|
payjoinEndpoint: string, |
|
|
|
|
|
): Promise<void> { |
|
|
|
|
|
return requestPayjoinWithCustomRemoteCall( |
|
|
|
|
|
psbt, |
|
|
|
|
|
(psbt1): Promise<Nullable<Psbt>> => doRequest(psbt1, payjoinEndpoint), |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function checkSanity(psbt: Psbt): { [index: number]: string[] } { |
|
|
function checkSanity(psbt: Psbt): { [index: number]: string[] } { |
|
|
const result: { [index: number]: string[] } = {}; |
|
|
const result: { [index: number]: string[] } = {}; |
|
|
psbt.data.inputs.forEach((value, index) => { |
|
|
psbt.data.inputs.forEach((value, index): void => { |
|
|
const sanityResult = checkInputSanity(value, getGlobalTransaction(psbt).ins[index]); |
|
|
const sanityResult = checkInputSanity( |
|
|
|
|
|
value, |
|
|
|
|
|
getGlobalTransaction(psbt).ins[index], |
|
|
|
|
|
); |
|
|
if (sanityResult.length > 0) { |
|
|
if (sanityResult.length > 0) { |
|
|
result[index] = sanityResult; |
|
|
result[index] = sanityResult; |
|
|
} |
|
|
} |
|
@ -94,7 +130,7 @@ function checkSanity(psbt: Psbt): { [index: number]: string[] } { |
|
|
return result; |
|
|
return result; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function checkInputSanity(input: PsbtInput, txInput: Input): string[] { |
|
|
function checkInputSanity(input: PsbtInput, txInput: TxInput): string[] { |
|
|
const errors: string[] = []; |
|
|
const errors: string[] = []; |
|
|
if (isFinalized(input)) { |
|
|
if (isFinalized(input)) { |
|
|
if (input.partialSig && input.partialSig.length > 0) { |
|
|
if (input.partialSig && input.partialSig.length > 0) { |
|
@ -126,36 +162,61 @@ function checkInputSanity(input: PsbtInput, txInput: Input): string[] { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (input.nonWitnessUtxo) { |
|
|
if (input.nonWitnessUtxo) { |
|
|
//TODO: get hash
|
|
|
// TODO: get hash
|
|
|
const prevOutTxId = input.nonWitnessUtxo; |
|
|
const prevOutTxId = input.nonWitnessUtxo; |
|
|
let validOutpoint = true; |
|
|
let validOutpoint = true; |
|
|
|
|
|
|
|
|
if (txInput.hash != prevOutTxId) { |
|
|
if (txInput.hash !== prevOutTxId) { |
|
|
errors.push('non_witness_utxo does not match the transaction id referenced by the global transaction sign'); |
|
|
errors.push( |
|
|
|
|
|
'non_witness_utxo does not match the transaction id referenced by the global transaction sign', |
|
|
|
|
|
); |
|
|
validOutpoint = false; |
|
|
validOutpoint = false; |
|
|
} |
|
|
} |
|
|
|
|
|
// @ts-ignore
|
|
|
if (txInput.index >= input.nonWitnessUtxo.Outputs.length) { |
|
|
if (txInput.index >= input.nonWitnessUtxo.Outputs.length) { |
|
|
errors.push('Global transaction referencing an out of bound output in non_witness_utxo'); |
|
|
errors.push( |
|
|
|
|
|
'Global transaction referencing an out of bound output in non_witness_utxo', |
|
|
|
|
|
); |
|
|
validOutpoint = false; |
|
|
validOutpoint = false; |
|
|
} |
|
|
} |
|
|
if (input.redeemScript && validOutpoint) { |
|
|
if (input.redeemScript && validOutpoint) { |
|
|
if (input.redeemScript.Hash.ScriptPubKey != input.nonWitnessUtxo.Outputs[txInput.index].ScriptPubKey) |
|
|
if ( |
|
|
errors.push('The redeem_script is not coherent with the scriptPubKey of the non_witness_utxo'); |
|
|
// @ts-ignore
|
|
|
|
|
|
input.redeemScript.Hash.ScriptPubKey !== |
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
input.nonWitnessUtxo.Outputs[txInput.index].ScriptPubKey |
|
|
|
|
|
) |
|
|
|
|
|
errors.push( |
|
|
|
|
|
'The redeem_script is not coherent with the scriptPubKey of the non_witness_utxo', |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (input.witnessUtxo) { |
|
|
if (input.witnessUtxo) { |
|
|
if (input.redeemScript) { |
|
|
if (input.redeemScript) { |
|
|
if (input.redeemScript.Hash.ScriptPubKey != input.witnessUtxo.ScriptPubKey) |
|
|
if ( |
|
|
errors.push('The redeem_script is not coherent with the scriptPubKey of the witness_utxo'); |
|
|
// @ts-ignore
|
|
|
if (input.witnessScript && |
|
|
input.redeemScript.Hash.ScriptPubKey !== input.witnessUtxo.ScriptPubKey |
|
|
|
|
|
) |
|
|
|
|
|
errors.push( |
|
|
|
|
|
'The redeem_script is not coherent with the scriptPubKey of the witness_utxo', |
|
|
|
|
|
); |
|
|
|
|
|
if ( |
|
|
|
|
|
input.witnessScript && |
|
|
input.redeemScript && |
|
|
input.redeemScript && |
|
|
PayToWitScriptHashTemplate.Instance.ExtractScriptPubKeyParameters(input.redeemScript) != input.witnessScript.WitHash) |
|
|
// @ts-ignore
|
|
|
errors.push('witnessScript with witness UTXO does not match the redeemScript'); |
|
|
PayToWitScriptHashTemplate.Instance.ExtractScriptPubKeyParameters( |
|
|
|
|
|
input.redeemScript, |
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
|
) !== input.witnessScript.WitHash |
|
|
|
|
|
) |
|
|
|
|
|
errors.push( |
|
|
|
|
|
'witnessScript with witness UTXO does not match the redeemScript', |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
//figure out how to port this lofic
|
|
|
// figure out how to port this lofic
|
|
|
// if (input.witnessUtxo.ScriptPubKey is Script s)
|
|
|
// if (input.witnessUtxo.ScriptPubKey is Script s)
|
|
|
// {
|
|
|
// {
|
|
|
//
|
|
|
//
|
|
@ -165,18 +226,24 @@ function checkInputSanity(input: PsbtInput, txInput: Input): string[] { |
|
|
// errors.push('A Witness UTXO is provided for a non-witness input');
|
|
|
// errors.push('A Witness UTXO is provided for a non-witness input');
|
|
|
// }
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return errors; |
|
|
return errors; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function hasKeypathInformationSet( |
|
|
function hasKeypathInformationSet(items: { bip32Derivation?: Bip32Derivation[] }[]): boolean { |
|
|
items: { bip32Derivation?: Bip32Derivation[] }[], |
|
|
return items.filter(value => value.bip32Derivation && value.bip32Derivation.length > 0).length > 0; |
|
|
): boolean { |
|
|
|
|
|
return ( |
|
|
|
|
|
items.filter( |
|
|
|
|
|
(value): boolean => |
|
|
|
|
|
!!value.bip32Derivation && value.bip32Derivation.length > 0, |
|
|
|
|
|
).length > 0 |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function isFinalized(input: PsbtInput) { |
|
|
function isFinalized(input: PsbtInput): boolean { |
|
|
return input.finalScriptSig !== undefined || |
|
|
return ( |
|
|
input.finalScriptWitness !== undefined; |
|
|
input.finalScriptSig !== undefined || input.finalScriptWitness !== undefined |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function getGlobalTransaction(psbt: Psbt): Transaction { |
|
|
function getGlobalTransaction(psbt: Psbt): Transaction { |
|
@ -186,14 +253,17 @@ function getGlobalTransaction(psbt: Psbt): Transaction { |
|
|
return psbt.__CACHE.__TX; |
|
|
return psbt.__CACHE.__TX; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function doRequest(psbt: Psbt, payjoinEndpoint: string): Promise<Nullable<Psbt>> { |
|
|
function doRequest( |
|
|
return new Promise<Nullable<Psbt>>((resolve, reject) => { |
|
|
psbt: Psbt, |
|
|
|
|
|
payjoinEndpoint: string, |
|
|
|
|
|
): Promise<Nullable<Psbt>> { |
|
|
|
|
|
return new Promise<Nullable<Psbt>>((resolve, reject): void => { |
|
|
if (!psbt) { |
|
|
if (!psbt) { |
|
|
reject(); |
|
|
reject(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const xhr = new XMLHttpRequest(); |
|
|
const xhr = new XMLHttpRequest(); |
|
|
xhr.onreadystatechange = () => { |
|
|
xhr.onreadystatechange = (): void => { |
|
|
if (xhr.readyState !== 4) return; |
|
|
if (xhr.readyState !== 4) return; |
|
|
if (xhr.status >= 200 && xhr.status < 300) { |
|
|
if (xhr.status >= 200 && xhr.status < 300) { |
|
|
resolve(Psbt.fromHex(xhr.responseText)); |
|
|
resolve(Psbt.fromHex(xhr.responseText)); |