Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xhliu/6c94d4fddbe8de6b0bf0c8db2baa98b3 to your computer and use it in GitHub Desktop.
Save xhliu/6c94d4fddbe8de6b0bf0c8db2baa98b3 to your computer and use it in GitHub Desktop.
TAALSigner
import React from 'react';
import { sendMessageAndWaitForResponse } from '../utils/globals';
import { connect } from '../store/portSlice';
import store, { RootState } from '../store/store';
import { TAALSigner } from '../signers/TAALSigner';
import {
bsv,
DefaultProvider,
DefaultProviderOption,
} from 'scrypt-ts'
type ConnectTAALWalletButtonProps = {
onConnected: (signer: TAALSigner) => void;
};
const ConnectTAALWalletButton: React.FC<ConnectTAALWalletButtonProps> = (props) => {
const connectWallet = async () => {
store.dispatch(connect());
const state: RootState = store.getState();
const port: RootState['port']['value'] = state.port.value;
if(!port){
console.error('Port is not defined');
} else
try {
const response = await sendMessageAndWaitForResponse(port, { action: 'connect' });
console.log('Received response:', response);
if(response.action === 'error'){
alert(response.payload.reason);
}
//Test code start
const responseBalance = await sendMessageAndWaitForResponse(port, { action: 'getBalance' });
console.log('connectWallet() getBalance:', responseBalance);
const responseUnspent = await sendMessageAndWaitForResponse(port, { action: 'getUnspent' });
console.log('connectWallet() getUnspent:', responseUnspent);
//Test code end
// https://github.com/sCrypt-Inc/tic-tac-toe/blob/main/src/App.tsx
const getNetworkResponse = await sendMessageAndWaitForResponse(port, { action: 'getNetwork' });
// const network = getNetworkResponse.payload;
const options: DefaultProviderOption = {
// taal: 'your-taal-api-key',
// gorillapool: 'your-gorillapool-api-key',
// sensible: 'your-sensible-api-key',
// scrypt: scryptConfig,
network: getNetworkResponse.payload, // You can use bsv.Networks.mainnet or bsv.Networks.testnet, or any other custom network
};
const provider = new DefaultProvider(options);
const signer = new TAALSigner(provider);
// Call the onConnected callback and pass the initialized signer
props.onConnected(signer);
} catch (error) {
if (error instanceof Error) {
console.error('Error:', error.message);
} else {
console.error('Error:', error);
}
}
};
return (
<button onClick={connectWallet}>
Connect TAAL Wallet
</button>
);
};
export default ConnectTAALWalletButton;
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 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;
}
}
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Define the initial state and its type
interface PortState {
value: chrome.runtime.Port | undefined;
}
const initialState: PortState = {
value: undefined,
};
// 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! },
);
},
},
});
// Export the actions and reducer
export const { connect } = portSlice.actions;
export default portSlice.reducer;
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;
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 } from '../utils/globals';
interface InputInfo {
inputIndex: number;
satoshis: number;
scriptHex: string;
}
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 {
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;
}
/**
* 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[]> {
throw new Error(`Method ${this.constructor.name}#getSignatures not implemented.`);
}
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