Skip to content

Commit 37da9ad

Browse files
mr-zwetsrkalis
andauthored
Add WalletConnect utils (#316)
Co-authored-by: Rosco Kalis <[email protected]>
1 parent 47a9dbc commit 37da9ad

File tree

7 files changed

+223
-76
lines changed

7 files changed

+223
-76
lines changed

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
binToHex,
33
decodeTransaction,
4+
decodeTransactionUnsafe,
45
encodeTransaction,
56
hexToBin,
67
Transaction as LibauthTransaction,
@@ -22,14 +23,17 @@ import { NetworkProvider } from './network/index.js';
2223
import {
2324
cashScriptOutputToLibauthOutput,
2425
createOpReturnOutput,
26+
generateLibauthSourceOutputs,
2527
validateInput,
2628
validateOutput,
2729
} from './utils.js';
2830
import { FailedTransactionError } from './Errors.js';
2931
import { DebugResults } from './debugging.js';
3032
import { getBitauthUri } from './LibauthTemplate.js';
3133
import { debugLibauthTemplate, getLibauthTemplates } from './advanced/LibauthTemplate.js';
34+
import { getWcContractInfo, WcSourceOutput, WcTransactionOptions } from './walletconnect-utils.js';
3235
import semver from 'semver';
36+
import { WcTransactionObject } from './walletconnect-utils.js';
3337

3438
export interface TransactionBuilderOptions {
3539
provider: NetworkProvider;
@@ -134,15 +138,7 @@ export class TransactionBuilder {
134138
};
135139

136140
// Generate source outputs from inputs (for signing with SIGHASH_UTXOS)
137-
const sourceOutputs = this.inputs.map((input) => {
138-
const sourceOutput = {
139-
amount: input.satoshis,
140-
to: input.unlocker.generateLockingBytecode(),
141-
token: input.token,
142-
};
143-
144-
return cashScriptOutputToLibauthOutput(sourceOutput);
145-
});
141+
const sourceOutputs = generateLibauthSourceOutputs(this.inputs);
146142

147143
const inputScripts = this.inputs.map((input, inputIndex) => (
148144
input.unlocker.generateUnlockingBytecode({ transaction, sourceOutputs, inputIndex })
@@ -226,4 +222,24 @@ export class TransactionBuilder {
226222
// Should not happen
227223
throw new Error('Could not retrieve transaction details for over 10 minutes');
228224
}
225+
226+
generateWcTransactionObject(options?: WcTransactionOptions): WcTransactionObject {
227+
const inputs = this.inputs;
228+
if (!inputs.every(input => isStandardUnlockableUtxo(input))) {
229+
throw new Error('All inputs must be StandardUnlockableUtxos to generate the wcSourceOutputs');
230+
}
231+
232+
const encodedTransaction = this.build();
233+
const transaction = decodeTransactionUnsafe(hexToBin(encodedTransaction));
234+
235+
const libauthSourceOutputs = generateLibauthSourceOutputs(inputs);
236+
const sourceOutputs: WcSourceOutput[] = libauthSourceOutputs.map((sourceOutput, index) => {
237+
return {
238+
...sourceOutput,
239+
...transaction.inputs[index],
240+
...getWcContractInfo(inputs[index]),
241+
};
242+
});
243+
return { ...options, transaction, sourceOutputs };
244+
}
229245
}

packages/cashscript/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export {
2121
MockNetworkProvider,
2222
} from './network/index.js';
2323
export { randomUtxo, randomToken, randomNFT } from './utils.js';
24+
export * from './walletconnect-utils.js';

packages/cashscript/src/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
LibauthOutput,
3333
TokenDetails,
3434
AddressType,
35+
UnlockableUtxo,
3536
} from './interfaces.js';
3637
import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js';
3738
import {
@@ -123,6 +124,19 @@ export function libauthOutputToCashScriptOutput(output: LibauthOutput): Output {
123124
};
124125
}
125126

127+
export function generateLibauthSourceOutputs(inputs: UnlockableUtxo[]): LibauthOutput[] {
128+
const sourceOutputs = inputs.map((input) => {
129+
const sourceOutput = {
130+
amount: input.satoshis,
131+
to: input.unlocker.generateLockingBytecode(),
132+
token: input.token,
133+
};
134+
135+
return cashScriptOutputToLibauthOutput(sourceOutput);
136+
});
137+
return sourceOutputs;
138+
}
139+
126140
function isTokenAddress(address: string): boolean {
127141
const result = decodeCashAddress(address);
128142
if (typeof result === 'string') throw new Error(result);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { StandardUnlockableUtxo, LibauthOutput, Unlocker } from './interfaces.js';
2+
import { type AbiFunction, type Artifact, scriptToBytecode } from '@cashscript/utils';
3+
import { cashAddressToLockingBytecode, type Input, type TransactionCommon } from '@bitauth/libauth';
4+
5+
// Wallet Connect interfaces according to the spec
6+
// see https://github.com/mainnet-pat/wc2-bch-bcr
7+
8+
export interface WcTransactionOptions {
9+
broadcast?: boolean;
10+
userPrompt?: string;
11+
}
12+
13+
export interface WcTransactionObject {
14+
transaction: TransactionCommon | string;
15+
sourceOutputs: WcSourceOutput[];
16+
broadcast?: boolean;
17+
userPrompt?: string;
18+
}
19+
20+
export type WcSourceOutput = Input & LibauthOutput & WcContractInfo;
21+
22+
export interface WcContractInfo {
23+
contract?: {
24+
abiFunction: AbiFunction;
25+
redeemScript: Uint8Array;
26+
artifact: Partial<Artifact>;
27+
}
28+
}
29+
30+
export function getWcContractInfo(input: StandardUnlockableUtxo): WcContractInfo | {} {
31+
// If the input does not have a contract unlocker, return an empty object
32+
if (!('contract' in input.unlocker)) return {};
33+
const contract = input.unlocker.contract;
34+
const abiFunctionName = input.unlocker.abiFunction?.name;
35+
const abiFunction = contract.artifact.abi.find(abi => abi.name === abiFunctionName);
36+
if (!abiFunction) {
37+
throw new Error(`ABI function ${abiFunctionName} not found in contract artifact`);
38+
}
39+
const wcContractObj: WcContractInfo = {
40+
contract: {
41+
abiFunction: abiFunction,
42+
redeemScript: scriptToBytecode(contract.redeemScript),
43+
artifact: contract.artifact,
44+
},
45+
};
46+
return wcContractObj;
47+
}
48+
49+
export const placeholderSignature = (): Uint8Array => Uint8Array.from(Array(65));
50+
export const placeholderPublicKey = (): Uint8Array => Uint8Array.from(Array(33));
51+
52+
export const placeholderP2PKHUnlocker = (userAddress: string): Unlocker => {
53+
const decodeAddressResult = cashAddressToLockingBytecode(userAddress);
54+
55+
if (typeof decodeAddressResult === 'string') {
56+
throw new Error(`Invalid address: ${decodeAddressResult}`);
57+
}
58+
59+
const lockingBytecode = decodeAddressResult.bytecode;
60+
return {
61+
generateLockingBytecode: () => lockingBytecode,
62+
generateUnlockingBytecode: () => Uint8Array.from(Array(0)),
63+
};
64+
};

website/docs/guides/walletconnect.md

Lines changed: 46 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,66 @@ Most relevant for smart contract usage is the BCH-WalletConnect `signTransaction
1616

1717
> This is a most generic interface to propose a bitcoincash transaction to a wallet which reconstructs it and signs it on behalf of the wallet user.
1818
19-
```typescript
20-
signTransaction: (
21-
options: {
22-
transaction: string | TransactionBCH,
23-
sourceOutputs: (Input | Output | ContractInfo)[],
24-
broadcast?: boolean,
25-
userPrompt?: string
26-
}
27-
) => Promise<{ signedTransaction: string, signedTransactionHash: string } | undefined>;
19+
```ts
20+
signTransaction: (wcTransactionObj: WcTransactionObject) => Promise<SignedTxObject | undefined>;
2821
```
2922

30-
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:
31-
32-
>To signal that the wallet needs to sign an input, the app sets the corresponding input's `unlockingBytecode` to empty Uint8Array.
23+
```ts
24+
interface WcTransactionObject {
25+
transaction: TransactionCommon | string;
26+
sourceOutputs: WcSourceOutput[];
27+
broadcast?: boolean;
28+
userPrompt?: string;
29+
}
3330

34-
Also important for smart contract usage is how the wallet adds the public-key or a signature to contract inputs:
31+
type WcSourceOutput = Input & Output & WcContractInfo;
3532

36-
> 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.
33+
interface WcContractInfo {
34+
contract?: {
35+
abiFunction: AbiFunction;
36+
redeemScript: Uint8Array;
37+
artifact: Partial<Artifact>;
38+
}
39+
}
3740

38-
## Create wcTransactionObj
41+
interface SignedTxObject {
42+
signedTransaction: string;
43+
signedTransactionHash: string;
44+
}
45+
```
3946

40-
To use the BCH WalletConnect `signTransaction` API, we need to pass an `options` object which we'll call `wcTransactionObj`.
47+
To use the BCH WalletConnect `signTransaction` API, we need to pass a `wcTransactionObj`.
48+
CashScript `TransactionBuilder` has a `generateWcTransactionObject` method for creating this object.
4149

42-
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`
50+
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`
4351

4452
### Spending a user-input
4553

4654
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).
4755

4856
```ts
49-
import { Contract } from "cashscript";
57+
import { TransactionBuilder, placeholderP2PKHUnlocker } from "cashscript";
5058
import { hexToBin, decodeTransaction } from "@bitauth/libauth";
5159

52-
async function proposeWcTransaction(){
53-
// create a placeholderUnlocker for the empty signature
54-
const placeholderUnlocker: Unlocker = {
55-
generateLockingBytecode: () => convertPkhToLockingBytecode(userPkh),
56-
generateUnlockingBytecode: () => Uint8Array.from(Array(0))
57-
}
60+
async function proposeWcTransaction(userAddress: string){
61+
// use a placeholderUnlocker which will be replaced by the user's wallet
62+
const placeholderUnlocker = placeholderP2PKHUnlocker(userAddress)
5863

59-
// use the CashScript SDK to build a transaction
64+
// use the CashScript SDK to construct a transaction
6065
const transactionBuilder = new TransactionBuilder({provider: store.provider})
6166
transactionBuilder.addInputs(userInputUtxos, placeholderUnlocker)
6267
transactionBuilder.addOpReturnOutput(opReturnData)
6368
transactionBuilder.addOutput(contractOutput)
6469
if(changeAmount > 550n) transactionBuilder.addOutput(changeOutput)
6570

66-
const unsignedRawTransactionHex = await transactionBuilder.build();
67-
68-
const decodedTransaction = decodeTransaction(hexToBin(unsignedRawTransactionHex));
69-
if(typeof decodedTransaction == "string") throw new Error("!decodedTransaction")
70-
71-
// construct SourceOutputs from transaction input, see source code
72-
const sourceOutputs = generateSourceOutputs(transactionBuilder.inputs)
73-
74-
// we don't need to add the contractInfo to the wcSourceOutputs here
75-
const wcSourceOutputs = sourceOutputs.map((sourceOutput, index) => {
76-
return { ...sourceOutput, ...decodedTransaction.inputs[index] }
77-
})
78-
79-
// wcTransactionObj to pass to signTransaction endpoint
80-
const wcTransactionObj = {
81-
transaction: decodedTransaction,
82-
sourceOutputs: listSourceOutputs,
71+
// Generate WalletConnect transaction object with custom 'broadcast' and 'userPrompt' options
72+
const wcTransactionObj = transactionBuilder.generateWcTransactionObject({
8373
broadcast: true,
84-
userPrompt: "Create HODL Contract"
85-
};
74+
userPrompt: "Create HODL Contract",
75+
});
8676

8777
// pass wcTransactionObj to WalletConnect client
78+
// (see signWcTransaction implementation below)
8879
const signResult = await signWcTransaction(wcTransactionObj);
8980

9081
// Handle signResult success / failure
@@ -96,41 +87,28 @@ async function proposeWcTransaction(){
9687
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).
9788

9889
```ts
99-
import { Contract } from "cashscript";
90+
import { TransactionBuilder, placeholderSignature, placeholderPublicKey } from "cashscript";
10091
import { hexToBin, decodeTransaction } from "@bitauth/libauth";
10192

10293
async function unlockHodlVault(){
103-
// create a placeholder for the unlocking arguments
104-
const placeholderSig = Uint8Array.from(Array(65))
105-
const placeholderPubKey = Uint8Array.from(Array(33));
94+
// We use a placeholder signature and public key so this can be filled in by the user's wallet
95+
const placeholderSig = placeholderSignature()
96+
const placeholderPubKey = placeholderPublicKey()
10697

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

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

113-
const unsignedRawTransactionHex = transactionBuilder.build();
114-
115-
const decodedTransaction = decodeTransaction(hexToBin(unsignedRawTransactionHex));
116-
if(typeof decodedTransaction == "string") throw new Error("!decodedTransaction")
117-
118-
const sourceOutputs = generateSourceOutputs(transactionBuilder.inputs)
119-
120-
// Add the contractInfo to the wcSourceOutputs
121-
const wcSourceOutputs: wcSourceOutputs = sourceOutputs.map((sourceOutput, index) => {
122-
const contractInfoWc = createWcContractObj(hodlContract, index)
123-
return { ...sourceOutput, ...contractInfoWc, ...decodedTransaction.inputs[index] }
124-
})
125-
126-
const wcTransactionObj = {
127-
transaction: decodedTransaction,
128-
sourceOutputs: wcSourceOutputs,
104+
// Generate WalletConnect transaction object with custom 'broadcast' and 'userPrompt' options
105+
const wcTransactionObj = transactionBuilder.generateWcTransactionObject({
129106
broadcast: true,
130107
userPrompt: "Reclaim HODL Value",
131-
};
108+
});
132109

133110
// pass wcTransactionObj to WalletConnect client
111+
// (see signWcTransaction implementation below)
134112
const signResult = await signWcTransaction(wcTransactionObj);
135113

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

150-
interface signedTxObject {
129+
interface SignedTxObject {
151130
signedTransaction: string;
152131
signedTransactionHash: string;
153132
}
154133

155-
async function signWcTransaction(wcTransactionObj): signedTxObject | undefined {
134+
async function signWcTransaction(wcTransactionObj: WcTransactionObject): SignedTxObject | undefined {
156135
try {
157136
const result = await signClient.request({
158137
chainId: connectedChain,

website/docs/releases/release-notes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
title: Release Notes
33
---
44

5+
## v0.11.1
6+
7+
#### CashScript SDK
8+
- :sparkles: Add `generateWcTransactionObject()` method to `TransactionBuilder` to generate a `WcTransactionObject` that can be used to sign a transaction with a WalletConnect client.
9+
- :sparkles: Add `placeholderSignature()`, `placeholderPublicKey()` and `placeholderP2PKHUnlocker()` helper functions to the SDK for WalletConnect usage.
10+
511
## v0.11.0
612

713
This update adds CashScript support for the new BCH 2025 network upgrade. To read more about the upgrade, see [this blog post](https://blog.bitjson.com/2025-chips/).

0 commit comments

Comments
 (0)