8000 Wc utils by mr-zwets · Pull Request #316 · CashScript/cashscript · GitHub
[go: up one dir, main page]

Skip to content

Wc utils #316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 8, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
change helperfunctions & update docs
  • Loading branch information
mr-zwets committed Jun 26, 2025
commit e5408a207513d3f05f803b5b5e9e816cbf3e1e05
42 changes: 0 additions & 42 deletions packages/cashscript/src/PlaceholderTemplate.ts

This file was deleted.

55 changes: 42 additions & 13 deletions packages/cashscript/src/wc-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { isStandardUnlockableUtxo } from './index.js';
import type { UnlockableUtxo, StandardUnlockableUtxo, LibauthOutput } from './interfaces.js';
import { isStandardUnlockableUtxo, TransactionBuilder } from './index.js';
import type { StandardUnlockableUtxo, LibauthOutput, Unlocker } from './interfaces.js';
import { generateLibauthSourceOutputs } from './utils.js';
import { type AbiFunction, type Artifact, scriptToBytecode } from '@cashscript/utils';
import type { Input, TransactionCommon } from '@bitauth/libauth';
import { cashAddressToLockingBytecode, decodeTransactionUnsafe, hexToBin, type Input, type TransactionCommon } from '@bitauth/libauth';

// Wallet Connect interfaces according to the spec
// see https://github.com/mainnet-pat/wc2-bch-bcr

export interface WcTransactionObject {
transaction: TransactionCommon | string;
sourceOutputs: WcSourceOutput[];
broadcast?: boolean;
userPrompt?: string;
}

export type WcSourceOutput = Input & LibauthOutput & WcContractInfo;

export interface WcContractInfo {
contract?: {
abiFunction: AbiFunction;
Expand All @@ -15,8 +24,6 @@ export interface WcContractInfo {
}
}

export type WcSourceOutputs = (Input & LibauthOutput & WcContractInfo)[];

function getWcContractInfo(input: StandardUnlockableUtxo): WcContractInfo | {} {
// If the input does not have a contract unlocker, return an empty object
if (!('contract' in input.unlocker)) return {};
Expand All @@ -26,7 +33,7 @@ function getWcContractInfo(input: StandardUnlockableUtxo): WcContractInfo | {} {
if (!abiFunction) {
throw new Error(`ABI function ${abiFunctionName} not found in contract artifact`);
}
const wcContractObj:WcContractInfo = {
const wcContractObj: WcContractInfo = {
contract: {
abiFunction: abiFunction,
redeemScript: scriptToBytecode(contract.redeemScript),
Expand All @@ -36,19 +43,41 @@ function getWcContractInfo(input: StandardUnlockableUtxo): WcContractInfo | {} {
return wcContractObj;
}

export function generateWcSourceOutputs(
inputs: UnlockableUtxo[], decodedTransaction:TransactionCommon,
): WcSourceOutputs {
export function generateWcTransactionObject(
transactionBuilder: TransactionBuilder,
): WcTransactionObject {
const inputs = transactionBuilder.inputs;
if (!inputs.every(input => isStandardUnlockableUtxo(input))) {
throw new Error('All inputs must be StandardUnlockableUtxos to generate the wcSourceOutputs');
}
const sourceOutputs = generateLibauthSourceOutputs(inputs);
const wcSourceOutputs: WcSourceOutputs = sourceOutputs.map((sourceOutput, index) => {

const encodedTransaction = transactionBuilder.build();
const transaction = decodeTransactionUnsafe(hexToBin(encodedTransaction));

const libauthSourceOutputs = generateLibauthSourceOutputs(inputs);
const sourceOutputs: WcSourceOutput[] = libauthSourceOutputs.map((sourceOutput, index) => {
return {
...sourceOutput,
...decodedTransaction.inputs[index],
...transaction.inputs[index],
...getWcContractInfo(inputs[index]),
};
});
return wcSourceOutputs;
return { transaction, sourceOutputs };
}

export const placeholderSignature = (): Uint8Array => Uint8Array.from(Array(65));
export const placeholderPublicKey = (): Uint8Array => Uint8Array.from(Array(33));

export const placeholderP2PKHUnlocker = (userAddress: string): Unlocker => {
const decodeAddressResult = cashAddressToLockingBytecode(userAddress);

if (typeof decodeAddressResult === 'string') {
throw new Error(`Invalid address: ${decodeAddressResult}`);
}

const lockingBytecode = decodeAddressResult.bytecode;
return {
generateLockingBytecode: () => lockingBytecode,
generateUnlockingBytecode: () => Uint8Array.from(Array(0)),
};
};
60 changes: 21 additions & 39 deletions website/docs/guides/walletconnect.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Most relevant for smart contract usage is the BCH-WalletConnect `signTransaction

```typescript
signTransaction: (
options: {
wcTransactionObj: {
transaction: string | TransactionBCH,
sourceOutputs: (Input | Output | ContractInfo)[],
broadcast?: boolean,
Expand All @@ -27,52 +27,40 @@ signTransaction: (
) => Promise<{ signedTransaction: string, signedTransactionHash: string } | undefined>;
```

The `transaction` passed in this object needs to be an unsigned transaction which is using placeholder values for the field that the user-wallet needs to fill in itself. You can use the `PlaceholderTemplate` class to generate the zero-placeholder values.
To use the BCH WalletConnect `signTransaction` API, we need to pass a `wcTransactionObj`.
CashScript `TransactionBuilder` has a handy helper function `generateWcTransactionObject` for creating this object.

The `sourceOutputs` value can be easily generated with the CashScript `generateWcSourceOutputs` helperfunction.

## Create wcTransactionObj

To use the BCH WalletConnect `signTransaction` API, we need to pass an `options` object which we'll call `wcTransactionObj`.

Below we'll give 2 example, the first example using spending a user-input and in the second example spending from a user-contract with the `userPubKey` and the `userSig`
Below we show 2 examples, the first example using spending a user-input and in the second example spending from a user-contract with placeholders for `userPubKey` and `userSig`

### Spending a user-input

Below is example code from the `CreateContract` code of the 'Hodl Vault' dapp repository, [link to source code](https://github.com/mr-zwets/bch-hodl-dapp/blob/main/src/views/CreateContract.vue#L14).

```ts
import { TransactionBuilder, generateWcSourceOutputs, placeholderTemplate } from "cashscript";
import { TransactionBuilder, generateWcTransactionObject, getPlaceholderP2PKHUnlocker } from "cashscript";
import { hexToBin, decodeTransaction } from "@bitauth/libauth";

async function proposeWcTransaction(userAddress: string){
// create a placeholderTemplate to generate placeholder unlocker
const placeholderTemplate = new PlaceholderTemplate(userAddress)
// use a placeholderUnlocker which will be replaced by the user's wallet
const placeholderUnlocker = getPlaceholderP2PKHUnlocker(userAddress)

// use the CashScript SDK to build a transaction
// use the CashScript SDK to construct a transaction
const transactionBuilder = new TransactionBuilder({provider: store.provider})
transactionBuilder.addInputs(userInputUtxos, placeholderTemplate.unlockP2PKH())
transactionBuilder.addInputs(userInputUtxos, placeholderUnlocker)
transactionBuilder.addOpReturnOutput(opReturnData)
transactionBuilder.addOutput(contractOutput)
if(changeAmount > 550n) transactionBuilder.addOutput(changeOutput)

const unsignedRawTransactionHex = await transactionBuilder.build();

const decodedTransaction = decodeTransaction(hexToBin(unsignedRawTransactionHex));
if(typeof decodedTransaction == "string") throw new Error("!decodedTransaction")

// construct WcSourceOutputs from transaction input using a CashScript helper function
const wcSourceOutputs = generateWcSourceOutputs(transactionBuilder.inputs)

// wcTransactionObj to pass to signTransaction endpoint
// Combine the generated WalletConnect transaction object with custom 'broadcast' and 'userPrompt' properties
const wcTransactionObj = {
transaction: decodedTransaction,
sourceOutputs: wcSourceOutputs,
...generateWcTransactionObject(transactionBuilder),
broadcast: true,
userPrompt: "Create HODL Contract"
};

// pass wcTransactionObj to WalletConnect client
// (see signWcTransaction implementation below)
const signResult = await signWcTransaction(wcTransactionObj);

// Handle signResult success / failure
Expand All @@ -84,37 +72,30 @@ async function proposeWcTransaction(userAddress: string){
Below is example code from the `unlockHodlVault` code of the 'Hodl Vault' dapp repository, [link to source code](https://github.com/mr-zwets/bch-hodl-dapp/blob/main/src/views/UserContracts.vue#L66).

```ts
import { TransactionBuilder, generateWcSourceOutputs, PlaceholderTemplate } from "cashscript";
import { TransactionBuilder, generateWcTransactionObject, placeholderSignature, getPlaceholderPubKey } from "cashscript";
import { hexToBin, decodeTransaction } from "@bitauth/libauth";

async function unlockHodlVault(){
// use placeholderTemplate to create placeholder args for unlocker
const placeholderTemplate = new PlaceholderTemplate()
const placeholderSig = placeholderTemplate.generateSignature()
const placeholderPubKey = placeholderTemplate.getPublicKey()
// We use a placeholder signatures and publickey so this can be filled in by the user's wallet
const placeholderSig = placeholderSignature()()
const placeholderPubKey = getPlaceholderPubKey()

const transactionBuilder = new TransactionBuilder({provider: store.provider})

transactionBuilder.setLocktime(store.currentBlockHeight)
transactionBuilder.addInputs(contractUtxos, hodlContract.unlock.spend(placeholderPubKey, placeholderSig))
transactionBuilder.addOutput(reclaimOutput)

const unsignedRawTransactionHex = transactionBuilder.build();

const decodedTransaction = decodeTransaction(hexToBin(unsignedRawTransactionHex));
if(typeof decodedTransaction == "string") throw new Error("!decodedTransaction")

// construct WcSourceOutputs from transaction input using a CashScript helper function
const wcSourceOutputs = generateWcSourceOutputs(transactionBuilder.inputs)

// Combine the generated WalletConnect transaction object with custom 'broadcast' and 'userPrompt' properties
const wcTransactionObj = {
transaction: decodedTransaction,
sourceOutputs: wcSourceOutputs,
...generateWcTransactionObject(transactionBuilder),
broadcast: true,
userPrompt: "Reclaim HODL Value",
};

// pass wcTransactionObj to WalletConnect client
// (see signWcTransaction implementation below)
const signResult = await signWcTransaction(wcTransactionObj);

// Handle signResult success / failure
Expand All @@ -130,13 +111,14 @@ See [the source code](https://github.com/mr-zwets/bch-hodl-dapp/blob/main/src/st
```ts
import SignClient from '@walletconnect/sign-client';
import { stringify } from "@bitauth/libauth";
import { type WcTransactionObject } from "cashscript";

interface signedTxObject {
signedTransaction: string;
signedTransactionHash: string;
}

async function signWcTransaction(wcTransactionObj): signedTxObject | undefined {
async function signWcTransaction(wcTransactionObj: WcTransactionObject): signedTxObject | undefined {
try {
const result = await signClient.request({
chainId: connectedChain,
Expand Down
0