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

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 2 additions & 9 deletions packages/cashscript/src/TransactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { NetworkProvider } from './network/index.js';
import {
cashScriptOutputToLibauthOutput,
createOpReturnOutput,
generateLibauthSourceOutputs,
validateInput,
validateOutput,
} from './utils.js';
Expand Down Expand Up @@ -134,15 +135,7 @@ export class TransactionBuilder {
};

// Generate source outputs from inputs (for signing with SIGHASH_UTXOS)
const sourceOutputs = this.inputs.map((input) => {
const sourceOutput = {
amount: input.satoshis,
to: input.unlocker.generateLockingBytecode(),
token: input.token,
};

return cashScriptOutputToLibauthOutput(sourceOutput);
});
const sourceOutputs = generateLibauthSourceOutputs(this.inputs);

const inputScripts = this.inputs.map((input, inputIndex) => (
input.unlocker.generateUnlockingBytecode({ transaction, sourceOutputs, inputIndex })
Expand Down
1 change: 1 addition & 0 deletions packages/cashscript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export {
MockNetworkProvider,
} from './network/index.js';
export { randomUtxo, randomToken, randomNFT } from './utils.js';
export * from './wc-utils.js';
14 changes: 14 additions & 0 deletions packages/cashscript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
LibauthOutput,
TokenDetails,
AddressType,
UnlockableUtxo,
} from './interfaces.js';
import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
import {
Expand Down Expand Up @@ -123,6 +124,19 @@ export function libauthOutputToCashScriptOutput(output: LibauthOutput): Output {
};
}

export function generateLibauthSourceOutputs(inputs: UnlockableUtxo[]): LibauthOutput[] {
const sourceOutputs = inputs.map((input) => {
const sourceOutput = {
amount: input.satoshis,
to: input.unlocker.generateLockingBytecode(),
token: input.token,
};

return cashScriptOutputToLibauthOutput(sourceOutput);
});
return sourceOutputs;
}

function isTokenAddress(address: string): boolean {
const result = decodeCashAddress(address);
if (typeof result === 'string') throw new Error(result);
Expand Down
83 changes: 83 additions & 0 deletions packages/cashscript/src/wc-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 { 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;
redeemScript: Uint8Array;
artifact: Partial<Artifact>;
}
}

function getWcContractInfo(input: StandardUnlockableUtxo): WcContractInfo | {} {
// If the input does not have a contract unlocker, return an empty object
if (!('contract' in input.unlocker)) return {};
const contract = input.unlocker.contract;
const abiFunctionName = input.unlocker.abiFunction?.name;
const abiFunction = contract.artifact.abi.find(abi => abi.name === abiFunctionName);
if (!abiFunction) {
throw new Error(`ABI function ${abiFunctionName} not found in contract artifact`);
}
const wcContractObj: WcContractInfo = {
contract: {
abiFunction: abiFunction,
redeemScript: scriptToBytecode(contract.redeemScript),
artifact: contract.artifact,
},
};
return wcContractObj;
}

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 encodedTransaction = transactionBuilder.build();
const transaction = decodeTransactionUnsafe(hexToBin(encodedTransaction));

const libauthSourceOutputs = generateLibauthSourceOutputs(inputs);
const sourceOutputs: WcSourceOutput[] = libauthSourceOutputs.map((sourceOutput, index) => {
return {
...sourceOutput,
...transaction.inputs[index],
...getWcContractInfo(inputs[index]),
};
});
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)),
};
};
76 changes: 21 additions & 55 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,64 +27,40 @@ signTransaction: (
) => Promise<{ signedTransaction: string, signedTransactionHash: string } | undefined>;
```

You can see that the CashScript `ContractInfo` needs to be provided as part of the `sourceOutputs`. Important to note from the spec is how the wallet knows which inputs to sign:
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.

>To signal that the wallet needs to sign an input, the app sets the corresponding input's `unlockingBytecode` to empty Uint8Array.

Also important for smart contract usage is how the wallet adds the public-key or a signature to contract inputs:

> We signal the use of pubkeys by using a 33-byte long zero-filled arrays and schnorr (the currently supported type) signatures by using a 65-byte long zero-filled arrays. Wallet detects these patterns and replaces them accordingly.

## 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 { Contract } from "cashscript";
import { TransactionBuilder, generateWcTransactionObject, getPlaceholderP2PKHUnlocker } from "cashscript";
import { hexToBin, decodeTransaction } from "@bitauth/libauth";

async function proposeWcTransaction(){
// create a placeholderUnlocker for the empty signature
const placeholderUnlocker: Unlocker = {
generateLockingBytecode: () => convertPkhToLockingBytecode(userPkh),
generateUnlockingBytecode: () => Uint8Array.from(Array(0))
}
async function proposeWcTransaction(userAddress: string){
// 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, 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 SourceOutputs from transaction input, see source code
const sourceOutputs = generateSourceOutputs(transactionBuilder.inputs)

// we don't need to add the contractInfo to the wcSourceOutputs here
const wcSourceOutputs = sourceOutputs.map((sourceOutput, index) => {
return { ...sourceOutput, ...decodedTransaction.inputs[index] }
})

// wcTransactionObj to pass to signTransaction endpoint
// Combine the generated WalletConnect transaction object with custom 'broadcast' and 'userPrompt' properties
const wcTransactionObj = {
transaction: decodedTransaction,
sourceOutputs: listSourceOutputs,
...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 @@ -96,41 +72,30 @@ async function proposeWcTransaction(){
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 { Contract } from "cashscript";
import { TransactionBuilder, generateWcTransactionObject, placeholderSignature, getPlaceholderPubKey } from "cashscript";
import { hexToBin, decodeTransaction } from "@bitauth/libauth";

async function unlockHodlVault(){
// create a placeholder for the unlocking arguments
const placeholderSig = Uint8Array.from(Array(65))
const placeholderPubKey = Uint8Array.from(Array(33));
// 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")

const sourceOutputs = generateSourceOutputs(transactionBuilder.inputs)

// Add the contractInfo to the wcSourceOutputs
const wcSourceOutputs: wcSourceOutputs = sourceOutputs.map((sourceOutput, index) => {
const contractInfoWc = createWcContractObj(hodlContract, index)
return { ...sourceOutput, ...contractInfoWc, ...decodedTransaction.inputs[index] }
})

// 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 @@ -146,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