Last active
August 11, 2023 21:42
-
-
Save Neggia/1b0485997d490040c613d4bd9ddc1cc3 to your computer and use it in GitHub Desktop.
TAALSigner
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { connect } from '../store/portSlice'; | |
import store, { RootState } from '../store/store'; | |
// import { sendMessageAndWaitForResponse } from '../utils/globals'; | |
// src/utils.ts | |
// import { Store } from 'redux'; // Assuming you are using Redux for state management | |
interface Port { | |
postMessage: (message: any) => void; | |
onMessage: { | |
addListener: (listener: (response: any) => void) => void; | |
removeListener: (listener: (response: any) => void) => void; | |
}; | |
} | |
interface QueueItem { | |
request: () => Promise<any>; | |
resolve: (response: any) => void; | |
reject: (error: any) => void; | |
} | |
const requestQueue: QueueItem[] = []; | |
const delayBetweenCalls = 400; //https://docs.taal.com/core-products/whatsonchain#rate-limits | |
const processQueue = async () => { | |
if (requestQueue.length === 0) { | |
return; | |
} | |
const currentItem = requestQueue[0]; | |
try { | |
const response = await currentItem.request(); | |
currentItem.resolve(response); | |
} catch (error) { | |
currentItem.reject(error); | |
} finally { | |
requestQueue.shift(); | |
processQueue(); | |
} | |
}; | |
let currentRequestId = 0; | |
export function sendMessageAndWaitForResponse(port: Port, message: any): Promise<any> { | |
return new Promise((resolve, reject) => { | |
const requestId = currentRequestId++; | |
message.requestId = requestId; | |
const request = async () => { | |
return new Promise((resolve, reject) => { | |
// Send the message | |
port.postMessage(message); | |
// Set up the response listener | |
function listener(response: any) { | |
if (response.requestId !== requestId) { | |
// Ignore the response if it doesn't match the request ID | |
return; | |
} | |
// Remove the listener after receiving the response | |
port.onMessage.removeListener(listener); | |
// Resolve the Promise with the response | |
resolve(response); | |
} | |
port.onMessage.addListener(listener); | |
// Set up a timeout to reject the Promise if there's no response after a specified time | |
setTimeout(() => { | |
// Remove the listener if it's still active | |
port.onMessage.removeListener(listener); | |
// Reject the Promise with an error | |
reject(new Error('Timeout waiting for response')); | |
}, 10000); // 10 seconds timeout | |
}); | |
}; | |
requestQueue.push({ request, resolve, reject }); | |
if (requestQueue.length === 1) { | |
processQueue(); | |
} | |
}); | |
} | |
// export async function sendAction(store: Store, action: string, payload: any): Promise<any> { | |
export async function sendAction(action: string, payload?: any): Promise<any> { | |
const state: RootState = store.getState(); | |
const port: RootState['port']['value'] = state.port.value; | |
if(!port){ | |
console.error('Port is not defined'); | |
} else | |
try{ | |
const message = payload ? { action, payload } : { action }; | |
const response = await sendMessageAndWaitForResponse(port, message); | |
if (response.action === 'error') { | |
throw new Error(response.payload.reason); | |
} | |
return response; | |
} catch (error) { | |
if (error instanceof Error) { | |
console.error('Error:', error.message); | |
} else { | |
console.error('Error:', error); | |
} | |
} | |
} | |
export function isTAALChromeWalletConnected(): boolean { | |
const state: RootState = store.getState(); | |
const port: RootState['port']['value'] = state.port.value; | |
if(port === undefined) { | |
return false; | |
} else { | |
return true; | |
} | |
} | |
export function sleep(ms: number) { | |
return new Promise((resolve) => setTimeout(resolve, ms)); | |
} | |
interface FetchTransactionOptions { | |
callQueue?: boolean; | |
raw?: boolean; | |
} | |
export async function fetchTransactionByHash(txid: string, options?: FetchTransactionOptions) { | |
const callQueue = options?.callQueue; | |
const raw = options?.raw; | |
let localStorageItemId: string; | |
if(raw){ | |
localStorageItemId = `txraw-${txid}`; | |
} else { | |
localStorageItemId = `tx-${txid}`; | |
} | |
// Check if the transaction is in the cache | |
const cachedTransaction = localStorage.getItem(localStorageItemId); | |
if (cachedTransaction) { | |
console.log('Tx fetched from cache: ', txid); | |
// Parse and return the cached transaction | |
if(raw){ | |
return cachedTransaction; | |
} else { | |
return JSON.parse(cachedTransaction); | |
} | |
} | |
if(callQueue){ | |
// Wait for the specified delay duration before making the API call | |
await sleep(delayBetweenCalls); | |
} | |
const state: RootState = store.getState(); | |
const network: RootState['port']['network'] = state.port.network; | |
let whatsonchainUrlNetwork: string = 'main'; | |
if(network === 'testnet'){ | |
whatsonchainUrlNetwork = 'test'; | |
} | |
let txUrl: string; | |
if(raw){ | |
txUrl = `https://api.whatsonchain.com/v1/bsv/${whatsonchainUrlNetwork}/tx/${txid}/hex`; | |
} else { | |
txUrl = `https://api.whatsonchain.com/v1/bsv/${whatsonchainUrlNetwork}/tx/hash/${txid}`; | |
} | |
const response = await fetch(txUrl); | |
console.log('fetchTransactionByHash() response', response); | |
// Cache the fetched transaction in localStorage | |
let transactionData; | |
if(raw){ | |
transactionData = await response.text(); | |
localStorage.setItem(localStorageItemId, transactionData); | |
} else { | |
transactionData = await response.json(); | |
localStorage.setItem(localStorageItemId, JSON.stringify(transactionData)); | |
} | |
return transactionData; | |
}; | |
export function isValidDate(date: Date) { | |
return !isNaN(date.getTime()); | |
} | |
export function isDefined<T>(value: T | undefined | null): value is T { | |
return value !== undefined && value !== null; | |
} | |
export function matchWithPlaceholder(strWithPlaceholder: string, strToMatch: string) { | |
// Replace placeholders with regex that matches any characters | |
let placeholderRegex = strWithPlaceholder.replace(/<.*?>/g, '.*'); | |
// Add '.*' to the end of the regex to allow for any characters at the end of strToMatch | |
placeholderRegex += '.*'; | |
// console.log('matchWithPlaceholder() placeholderRegex', placeholderRegex); | |
// console.log('matchWithPlaceholder() strToMatch', strToMatch); | |
// Create a new RegExp with the placeholder regex | |
let regex = new RegExp(`^${placeholderRegex}$`); | |
// Test if the string matches the regex | |
const matchWithPlaceholder = regex.test(strToMatch); | |
// console.log('matchWithPlaceholder() matchWithPlaceholder', matchWithPlaceholder); | |
return matchWithPlaceholder; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createSlice } from '@reduxjs/toolkit'; | |
// Define the initial state and its type | |
interface PortState { | |
value: chrome.runtime.Port | undefined; | |
network: string; | |
} | |
const initialState: PortState = { | |
value: undefined, | |
network: 'main', | |
}; | |
// Create the slice | |
const portSlice = createSlice({ | |
name: 'port', | |
initialState, | |
reducers: { | |
connect: (state) => { | |
state.value = chrome.runtime.connect( | |
process.env.REACT_APP_TAAL_EXTENSION_ID!, | |
{ name: process.env.REACT_APP_NAME! }, | |
); | |
}, | |
setNetwork: (state, action) => { | |
state.network = action.payload; | |
}, | |
}, | |
}); | |
// Export the actions and reducer | |
export const { connect, setNetwork } = portSlice.actions; | |
export default portSlice.reducer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { configureStore } from '@reduxjs/toolkit'; | |
import portReducer from './portSlice'; | |
const store = configureStore({ | |
reducer: { | |
port: portReducer, | |
}, | |
}); | |
// Define the RootState type | |
export type RootState = ReturnType<typeof store.getState>; | |
export default store; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
bsv, | |
Signer, | |
SignTransactionOptions, | |
SignatureRequest, | |
SignatureResponse, | |
Provider | |
} from 'scrypt-ts'; | |
import { parseAddresses } from 'scrypt-ts/dist/bsv/utils'; | |
import AddressOption = bsv.Address //Z:\Projects\VSCodium\auoz\node_modules\scrypt-ts\dist\bsv\types.d.ts | |
import { Transaction, PublicKey, Script } from 'bsv'; | |
import { isTAALChromeWalletConnected, sendAction, fetchTransactionByHash, isDefined } from '../utils/globals'; | |
interface InputInfo { | |
inputIndex: number; | |
satoshis: number; | |
scriptHex: string; | |
} | |
interface signPreimagePayload{ | |
tx: string; | |
sighash: number; | |
script: string; | |
i: number; | |
satoshis: number; | |
} | |
interface inputSignature{ | |
inputIndex: number; | |
sigtype: number; | |
publicKey: bsv.PublicKey; | |
signature: bsv.crypto.Signature; | |
} | |
const DEFAULT_SIGHASH_TYPE = bsv.crypto.Signature.ALL; //Z:\Projects\VSCodium\auoz\node_modules\scrypt-ts\dist\bsv\signers\sensilet-signer.js | |
export class TAALSigner extends Signer { | |
override async isAuthenticated(): Promise<boolean>{ | |
const isTAALConnected: boolean = isTAALChromeWalletConnected(); | |
return isTAALConnected; | |
} | |
override async requestAuth(): Promise<{ | |
isAuthenticated: boolean; | |
error: string; | |
}> { | |
const isTAALConnected: boolean = isTAALChromeWalletConnected(); | |
const errorString: string = 'TAAL chrome wallet is not connected!'; | |
return { | |
isAuthenticated: isTAALConnected, | |
error: errorString | |
}; | |
} | |
override async connect(provider: Provider): Promise<this> { | |
// we should make sure TAAL chrome wallet is connected before we connect a provider. | |
const isTAALConnected: boolean = isTAALChromeWalletConnected(); | |
if(!isTAALConnected) { | |
Promise.reject(new Error('TAAL chrome wallet is not connected!')) | |
} | |
if(!provider.isConnected()) { | |
// connect the provider | |
await provider.connect(); | |
} | |
this.provider = provider; | |
return this; | |
} | |
override async getDefaultAddress(): Promise<bsv.Address> { | |
const response = await sendAction('getAddress'); | |
return bsv.Address.fromString(response.payload); | |
} | |
override async getDefaultPubKey(): Promise<PublicKey> { | |
// Real code start | |
const response = await sendAction('getRootPublicKey'); | |
const rootPublicKey = new bsv.PublicKey(response.payload); | |
// Real code end | |
// Test code start | |
/* const address = await this.getDefaultAddress(); | |
const rootPublicKey = await this.getPubKey(address); */ | |
// Test code end | |
console.log("getDefaultPubKey().toString(): ", rootPublicKey.toString()); | |
return Promise.resolve(rootPublicKey); | |
} | |
override async getPubKey(address: AddressOption): Promise<PublicKey> { | |
const response = await sendAction('getPublicKey'); | |
const publicKey = new bsv.PublicKey(response.payload); | |
console.log("getPubKey().toString(): ", publicKey.toString()); | |
return Promise.resolve(publicKey); | |
} | |
override async signRawTransaction(rawTxHex: string, options: SignTransactionOptions): Promise<string> { | |
// convert `rawTxHex` to a transation object | |
const sigReqsByInputIndex: Map<number, SignatureRequest> = (options?.sigRequests || []).reduce((m, sigReq) => { m.set(sigReq.inputIndex, sigReq); return m; }, new Map()); | |
const tx = new bsv.Transaction(rawTxHex); | |
tx.inputs.forEach((_, inputIndex) => { | |
const sigReq = sigReqsByInputIndex.get(inputIndex); | |
if (!sigReq) { | |
throw new Error(`\`SignatureRequest\` info should be provided for the input ${inputIndex} to call #signRawTransaction`) | |
} | |
const script = sigReq.scriptHex ? new bsv.Script(sigReq.scriptHex) : bsv.Script.buildPublicKeyHashOut(sigReq.address.toString()); | |
// set ref output of the input | |
tx.inputs[inputIndex].output = new bsv.Transaction.Output({ | |
script, | |
satoshis: sigReq.satoshis | |
}) | |
}); | |
const signedTx = await this.signTransaction(tx, options); | |
return signedTx.toString(); | |
} | |
public async getNetwork() { | |
const response = await sendAction('getNetwork'); | |
const network = response.payload; | |
console.log('network:', network); | |
return network; | |
} | |
updateInputsWithInfo(tx: Transaction, inputInfos: InputInfo[]): Transaction { | |
tx.inputs.forEach((input, index) => { | |
// Find the corresponding inputInfo based on the inputIndex | |
const inputInfo = inputInfos.find((info) => info.inputIndex === index); | |
if (inputInfo) { | |
// Update the input properties using the inputInfo data | |
input.output = new bsv.Transaction.Output({ | |
satoshis: inputInfo.satoshis, | |
script: Script.fromHex(inputInfo.scriptHex), | |
}); | |
} | |
}); | |
return tx; | |
} | |
override async signTransaction(tx: Transaction, options?: SignTransactionOptions): Promise<Transaction> { | |
// console.log("signTransaction() tx: ", tx); | |
// console.log("signTransaction() options: ", options); | |
const network = await this.getNetwork(); | |
const address = await this.getDefaultAddress(); | |
// console.log("signTransaction() address.toString(): ",address.toString()); | |
// Generate default `sigRequests` if not passed by user | |
const sigRequests: SignatureRequest[] = options?.sigRequests?.length ? options.sigRequests : | |
tx.inputs.map((input, inputIndex) => { | |
const useAddressToSign = options && options.address ? options.address : | |
input.output?.script.isPublicKeyHashOut() | |
? input.output.script.toAddress(network) | |
: address; | |
// : this._address; //this._address = scryptlib_1.bsv.Address.fromString(addr); in getConnectedTarget() | |
/* console.log("signTransaction() input.output?.script.isPublicKeyHashOut(): ",input.output?.script.isPublicKeyHashOut()); | |
console.log("signTransaction() input.output.script.toAddress(network).toString(): ",input.output?.script.toAddress(network).toString()); | |
console.log("signTransaction() address.toString(): ",address.toString()); | |
console.log("signTransaction() useAddressToSign.toString(): ",useAddressToSign.toString()); */ | |
return { | |
prevTxId: input.prevTxId.toString(), | |
outputIndex: input.outputIndex, | |
inputIndex, | |
satoshis: input.output?.satoshis ?? 0, //satoshis: input.output?.satoshis, | |
address: useAddressToSign, | |
scriptHex: input.output?.script?.toHex() ?? '', //scriptHex: input.output?.script?.toHex(), | |
sigHashType: DEFAULT_SIGHASH_TYPE, | |
} | |
}) | |
// console.log("signTransaction() sigRequests: ",sigRequests); | |
// Test code start | |
/* const rootPublicKey = await this.getDefaultPubKey(); | |
const addressFromRootPublicKey = bsv.Address.fromPublicKey(rootPublicKey, network); | |
console.log("signTransaction() rootPublicKey address.toString(): ",addressFromRootPublicKey.toString()); | |
const publicKey = await this.getPubKey(address); | |
const addressFromPublicKey = bsv.Address.fromPublicKey(publicKey, network); | |
console.log("signTransaction() publicKey address.toString(): ",addressFromPublicKey.toString()); | |
const isPublicKeyCorrect = tx.inputs.some(input => { | |
const inputScript = input.output?.script; | |
if (inputScript?.isPublicKeyHashOut()) { | |
const inputAddress = inputScript.toAddress(network); | |
console.log('signTransaction() inputAddress.toString(): ', inputAddress.toString()); | |
console.log('signTransaction() address.toString(): ', address.toString()); | |
return inputAddress.toString() === address.toString(); | |
} | |
return false; | |
}); | |
console.log('signTransaction() isPublicKeyCorrect: ', isPublicKeyCorrect); | |
const privateKeyWIF: string = process.env.REACT_APP_TAAL_PRIVATE_KEY_WIF as string; | |
const privateKey = bsv.PrivateKey.fromWIF(privateKeyWIF); | |
const addressFromPrivateKeyWIF = privateKey.toAddress(network).toString(); | |
console.log('signTransaction() addressFromPrivateKeyWIF:', addressFromPrivateKeyWIF); */ | |
// Test code end | |
const rawTxHex = tx.toString(); | |
const inputInfos = sigRequests.flatMap((sigReq) => { | |
const addresses = parseAddresses(sigReq.address, network); | |
return addresses.map(address => { | |
return { | |
txHex: rawTxHex, | |
inputIndex: sigReq.inputIndex, | |
scriptHex: sigReq.scriptHex || bsv.Script.buildPublicKeyHashOut(address).toHex(), | |
satoshis: sigReq.satoshis, | |
sigtype: sigReq.sigHashType || DEFAULT_SIGHASH_TYPE, | |
address: address.toString() | |
} | |
}); | |
}); | |
// console.log("signTransaction() inputInfos: ",inputInfos); | |
// Modify the transaction object using the inputInfos data | |
const updatedTx = this.updateInputsWithInfo(tx, inputInfos); | |
// console.log("signTransaction() updatedTx: ",updatedTx); | |
const response = await sendAction('signTx', updatedTx); | |
const signedTxHex = response.payload; | |
// console.log("signTransaction() signedTxHex: ",signedTxHex); | |
// Create a new Transaction object from the signed transaction hex | |
const signedTx = new bsv.Transaction(signedTxHex); | |
// console.log("signTransaction() signedTx: ",signedTx); | |
// Modify the transaction object using the inputInfos data | |
const updatedSignedTx = this.updateInputsWithInfo(signedTx, inputInfos); | |
console.log("signTransaction() updatedSignedTx.hash: ",updatedSignedTx.hash); | |
return updatedSignedTx; | |
} | |
async processPrevTxs(rawTxHex: string): Promise<Transaction> { | |
let rawTx = new bsv.Transaction(rawTxHex); | |
// Extract prevTxId for each input and fetch the transactions | |
const prevTxIds = rawTx.inputs.map(input => input.prevTxId.toString('hex')); | |
// Fetch all previous transactions using Promise.all | |
const prevTxs = await Promise.all(prevTxIds.map(txId => fetchTransactionByHash(txId, { callQueue: true }))); | |
for (const [index, prevTx] of prevTxs.entries()) { | |
const outputIndex = rawTx.inputs[index].outputIndex; | |
const script = Script.fromBuffer(Buffer.from(prevTx.vout[outputIndex].scriptPubKey.hex, 'hex')); | |
// const scriptHex = script.toHex(); | |
const satoshis: number = prevTx.vout[outputIndex].value * 100000000; | |
const newOutput = new Transaction.Output({ script: script, satoshis: satoshis }); | |
rawTx.inputs[index].output = newOutput; | |
/* if (script.isPublicKeyHashOut()){ | |
// t = new m.PublicKeyHash(e); | |
} else if (script.isScriptHashOut()) // && e.publicKeys && e.threshold) | |
{ | |
// t = new m.MultiSigScriptHash(e,e.publicKeys,e.threshold,e.signatures); | |
} else if (!script.isPublicKeyOut()) { | |
const sigtype = bsv.crypto.Signature.SIGHASH_ALL; //Check | |
const preimage = rawTx.getPreimage(index); | |
console.log("processPrevTxs() preimage: ", preimage); | |
const signPreimagePayload: signPreimagePayload = { | |
tx: rawTxHex, | |
sighash: sigtype, | |
script: scriptHex, | |
i: index, | |
satoshis: satoshis | |
}; | |
const signPreimageResponse = await sendAction('signPreimage', signPreimagePayload); | |
console.log("processPrevTxs() signPreimageResponse: ", signPreimageResponse); | |
} */ | |
} | |
return rawTx; | |
} | |
hasCustomScript(rawTx: Transaction): boolean{ | |
for (const [index, input] of rawTx.inputs.entries()) { | |
if (input.output?.script.isPublicKeyHashOut()){ | |
// t = new m.PublicKeyHash(e); | |
} else if (input.output?.script.isScriptHashOut()) // && e.publicKeys && e.threshold) | |
{ | |
// t = new m.MultiSigScriptHash(e,e.publicKeys,e.threshold,e.signatures); | |
} else if (!input.output?.script.isPublicKeyOut()) { | |
return true; | |
} | |
} | |
return false; | |
} | |
async signCustomInput(inputTx: Transaction, inputIndex: number): Promise<inputSignature> { | |
const rawTxHex = inputTx.toString(); | |
const sigtype = bsv.crypto.Signature.SIGHASH_ALL | bsv.crypto.Signature.SIGHASH_FORKID; | |
// const outputIndex = inputTx.inputs[inputIndex].outputIndex; | |
const script = inputTx?.inputs[inputIndex]?.output?.script; //Script.fromBuffer(Buffer.from(prevTx.vout[outputIndex].scriptPubKey.hex, 'hex')); | |
const scriptHex: string = (script ?? new bsv.Script('')).toHex(); | |
const satoshis: number = inputTx?.inputs[inputIndex]?.output?.satoshis ?? 0; | |
const rootPublicKey = await this.getDefaultPubKey(); | |
const preimage = inputTx.getPreimage(inputIndex); | |
console.log("signCustomInput() preimage: ", preimage); | |
const signPreimagePayload: signPreimagePayload = { | |
tx: rawTxHex, | |
sighash: sigtype, | |
script: scriptHex, | |
i: inputIndex, | |
satoshis: satoshis | |
}; | |
const signPreimageResponse = await sendAction('signPreimage', signPreimagePayload); | |
console.log("signCustomInput() signPreimageResponse: ", signPreimageResponse); | |
const signatureHex = signPreimageResponse.payload; | |
const signature = bsv.crypto.Signature.fromTxFormat(Buffer.from(signatureHex, 'hex')); | |
return { | |
inputIndex: inputIndex, | |
sigtype: sigtype, | |
publicKey: rootPublicKey, | |
signature: signature | |
} | |
} | |
async signStandardInput(inputTx: Transaction, inputIndex: number): Promise<inputSignature> { | |
const sigtype = bsv.crypto.Signature.SIGHASH_ALL | bsv.crypto.Signature.SIGHASH_FORKID; | |
const rootPublicKey = await this.getDefaultPubKey(); | |
const signedTx = await this.signTransaction(inputTx); | |
const script = signedTx.inputs[inputIndex].script; | |
const signature = script.chunks[0].buf; | |
console.log("signStandardInput() signature: ", signature); | |
const sig = signedTx.getSignature(inputIndex) as string; | |
console.log("signStandardInput() sig: ", sig); | |
// const signatureHex = signature.toString('hex'); | |
return { | |
inputIndex: inputIndex, | |
sigtype: sigtype, | |
publicKey: rootPublicKey, | |
signature: bsv.crypto.Signature.fromTxFormat(signature), | |
} | |
} | |
// For test purposes only | |
// Sign a transaction with inputs, one of which is a custom output script (Scrypt contract) and other are standard scripts (P2PKH or P2SH) | |
async signMixedTransaction(rawTx: Transaction): Promise<Transaction> { | |
// Filter the inputs for the custom and standard input transactions | |
let customInput; | |
let customInputIndex: number = -1; | |
const address = await this.getDefaultAddress(); | |
for (const [index, input] of rawTx.inputs.entries()) { | |
if (input.output?.script.isPublicKeyHashOut()){ | |
} else if (input.output?.script.isScriptHashOut()) // && e.publicKeys && e.threshold) | |
{ | |
} else if (!input.output?.script.isPublicKeyOut()) { | |
customInput = rawTx.inputs[index]; | |
customInputIndex = index; | |
} | |
} | |
const standardInputs = rawTx.inputs.filter((_, index) => index !== customInputIndex); | |
// Convert custom input to a UTXO object | |
if ( | |
isDefined(customInput) && | |
isDefined(customInput.prevTxId) && | |
isDefined(customInput.outputIndex) && | |
isDefined(customInput.script) && | |
isDefined(customInput.output) && | |
isDefined(customInput.output.script) && | |
isDefined(customInput.output.satoshis) | |
) { | |
const customInputUtxo: Transaction.IUnspentOutput = { | |
txId: customInput.prevTxId.toString('hex'), | |
outputIndex: customInput.outputIndex, | |
address: address.toString(), //customInput.script.toAddress().toString(), | |
script: customInput.output.script.toHex(), | |
satoshis: customInput.output.satoshis, | |
}; | |
// Create a transaction with the custom input | |
const customInputTx = new bsv.Transaction().from([customInputUtxo]); | |
// Sign the custom input using the function that signs the preimage of an individual input | |
const customInputSignature: inputSignature = await this.signCustomInput(customInputTx, customInputIndex); | |
// Apply the custom input signature to the original transaction | |
rawTx.applySignature(customInputSignature); | |
} | |
// Sign each standard input separately and apply the signature to the original transaction | |
for (const [index, standardInput] of standardInputs.entries()) { | |
if ( | |
isDefined(standardInput.output) && | |
isDefined(standardInput.output.script) && | |
isDefined(standardInput.output.satoshis) | |
) { | |
const standardInputUtxo = { | |
txId: standardInput.prevTxId.toString('hex'), | |
outputIndex: standardInput.outputIndex, | |
address: address.toString(), //standardInput.script.toAddress().toString(), | |
script: standardInput.output.script.toHex(), | |
satoshis: standardInput.output.satoshis, | |
}; | |
const standardInputTx = new bsv.Transaction().from([standardInputUtxo]); | |
const standardInputSignature = await this.signStandardInput(standardInputTx, index); | |
standardInputSignature.inputIndex = 1 + index; | |
rawTx.applySignature(standardInputSignature); | |
} | |
} | |
return rawTx; | |
} | |
// Sign a transaction with inputs, one of which is a custom output script (Scrypt contract) and other are standard scripts (P2PKH or P2SH) | |
async signMixedTransaction2(rawTx: Transaction): Promise<Transaction> { | |
// const address = await this.getDefaultAddress(); | |
// Sign each standard input separately and apply the signature to the original transaction | |
for (const [index, input] of rawTx.inputs.entries()) { | |
/* const inputUtxo = { | |
txId: input.prevTxId.toString('hex'), | |
outputIndex: input.outputIndex, | |
address: address.toString(), | |
script: input?.output?.script.toHex() ?? '', | |
satoshis: input?.output?.satoshis ?? 0, | |
}; | |
// Create a transaction with the custom input | |
const inputTx = new bsv.Transaction().from([inputUtxo]); */ | |
// Sign the custom input using the function that signs the preimage of an individual input | |
const inputSignature: inputSignature = await this.signCustomInput(rawTx, index); | |
// Apply the custom input signature to the original transaction | |
// rawTx.applySignature(inputSignature); | |
// Apply the signature to the input | |
const sigScript = bsv.Script.empty() | |
.add(inputSignature.signature.toTxFormat()) | |
.add(Buffer.from((inputSignature.sigtype).toString(16), 'hex')) | |
.add(inputSignature.publicKey.toBuffer()); | |
input.setScript(sigScript); | |
console.log(`signMixedTransaction2() rawTx.inputs[${index}].script.chunks[0].buf`, rawTx.inputs[index].script.chunks[0].buf.toJSON()); | |
console.log(`signMixedTransaction2() rawTx.inputs[${index}].script.chunks[1].buf`, rawTx.inputs[index].script.chunks[1].buf.toJSON()); | |
} | |
return rawTx; | |
} | |
// For test purposes only, sign with private key | |
async signMixedTransaction3(rawTx: Transaction): Promise<Transaction> { | |
// const address = await this.getDefaultAddress(); | |
const privateKeyWIF: string = process.env.REACT_APP_TAAL_PRIVATE_KEY_WIF as string; | |
const privateKey = bsv.PrivateKey.fromWIF(privateKeyWIF); | |
// const addressFromPrivateKeyWIF = privateKey.toAddress(network).toString(); | |
const sigtype = bsv.crypto.Signature.SIGHASH_ALL | bsv.crypto.Signature.SIGHASH_FORKID; | |
// const rootPublicKey = await this.getDefaultPubKey(); | |
// Sign each standard input separately and apply the signature to the original transaction | |
for (const [index, input] of rawTx.inputs.entries()) { | |
if(input.output){ | |
const satoshis = bsv.crypto.BN.fromNumber(input.output.satoshis); | |
const signature = bsv.Transaction.Sighash.sign(rawTx, privateKey, sigtype, index, input.output.script, satoshis); | |
console.log(`signMixedTransaction3() signature${index}`, signature.toString()); | |
const publicKey = bsv.PublicKey.fromPrivateKey(privateKey); //rootPublicKey | |
console.log(`signMixedTransaction3() publicKey${index}`, publicKey.toString()); | |
// Apply the signature to the input | |
const sigScript = bsv.Script.empty() | |
.add(signature.toTxFormat()) | |
.add(Buffer.from((sigtype).toString(16), 'hex')) | |
.add(publicKey.toBuffer()); | |
input.setScript(sigScript); | |
} | |
// console.log(`signMixedTransaction2() rawTx.inputs[${index}].script.chunks[0].buf`, rawTx.inputs[index].script.chunks[0].buf.toJSON()); | |
// console.log(`signMixedTransaction2() rawTx.inputs[${index}].script.chunks[1].buf`, rawTx.inputs[index].script.chunks[1].buf.toJSON()); | |
} | |
// Check if transaction is fully signed | |
if (rawTx.verify()) { | |
return rawTx; | |
} else { | |
throw new Error(`Transcation signing fail`); | |
} | |
} | |
/** | |
* Get signatures with api | |
* @param rawTxHex a transation raw hex | |
* @param sigRequests a `SignatureRequest` array for the some inputs of the transaction. | |
* @returns a `SignatureResponse` array | |
*/ | |
async getSignatures(rawTxHex: string, sigRequests: SignatureRequest[]): Promise<SignatureResponse[]> { | |
const rawTx = await this.processPrevTxs(rawTxHex); | |
console.log("getSignatures() rawTx: ", rawTx); | |
const hasCustomScript = this.hasCustomScript(rawTx); | |
let signedTx: Transaction; | |
if(hasCustomScript){ | |
signedTx = await this.signMixedTransaction2(rawTx); | |
} else { | |
signedTx = await this.signTransaction(rawTx); | |
} | |
// const rootPublicKey = await this.getDefaultPubKey(); | |
const address = await this.getDefaultAddress(); | |
const publicKey = await this.getPubKey(address); | |
const sigResponses = signedTx.inputs.map((input, idx) => { | |
const script = input.script; | |
const signature = script.chunks[0].buf; | |
// const sig = signedTx.getSignature(idx) as string; | |
const signatureHex = signature.toString('hex'); | |
// const publicKeyHex = script.chunks[1].buf.toString('hex'); | |
const publicKeyHex = publicKey.toString(); | |
const sigHashType = signature.readUInt8(signature.length - 1); | |
return { | |
inputIndex: idx, | |
sig: signatureHex, | |
publicKey: publicKeyHex, | |
sigHashType: sigHashType || DEFAULT_SIGHASH_TYPE | |
} | |
}) | |
console.log("getSignatures() sigResponses: ", sigResponses); | |
return sigResponses; | |
} | |
override async signMessage(message: string, address?: AddressOption): Promise<string> { | |
if (address) { | |
throw new Error(`${this.constructor.name}#signMessge with \`address\` param is not supported!`); | |
} | |
const response = await sendAction('signMessage', message); | |
return response.payload; //TODO | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment