Skip to content

Commit fbb66cf

Browse files
committed
add generateWizardConnectTransactionObject
1 parent cd1c745 commit fbb66cf

6 files changed

Lines changed: 341 additions & 60 deletions

File tree

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import {
1818
isStandardUnlockableUtxo,
1919
StandardUnlockableUtxo,
2020
VmResourceUsage,
21-
isContractUnlocker,
22-
BchChangeOutputOptions,
23-
TokenChangeOutputOptions,
24-
} from './interfaces.js';
21+
isContractUnlocker,
22+
BchChangeOutputOptions,
23+
TokenChangeOutputOptions,
24+
isPlaceholderUnlocker,
25+
} from './interfaces.js';
2526
import { NetworkProvider } from './network/index.js';
2627
import {
2728
calculateDust,
@@ -36,7 +37,13 @@ import {
3637
import { FailedTransactionError } from './Errors.js';
3738
import { DebugResults } from './debugging.js';
3839
import { debugLibauthTemplate, getLibauthTemplate, getBitauthUri } from './libauth-template/LibauthTemplate.js';
39-
import { getWcContractInfo, WcSourceOutput, WcTransactionOptions } from './walletconnect-utils.js';
40+
import {
41+
getWcContractInfo,
42+
WcSourceOutput,
43+
WcTransactionOptions,
44+
WizardConnectInputPath,
45+
WizardConnectTransactionObject,
46+
} from './walletconnect-utils.js';
4047
import semver from 'semver';
4148
import { WcTransactionObject } from './walletconnect-utils.js';
4249

@@ -561,9 +568,9 @@ export class TransactionBuilder {
561568
* @returns A WalletConnect transaction object ready to be sent to a WalletConnect wallet.
562569
* @throws If the transaction cannot be built (fee exceeds limit or fungible tokens burned).
563570
*/
564-
generateWcTransactionObject(options?: WcTransactionOptions): WcTransactionObject {
565-
const encodedTransaction = this.build();
566-
const transaction = decodeTransactionUnsafe(hexToBin(encodedTransaction));
571+
generateWcTransactionObject(options?: WcTransactionOptions): WcTransactionObject {
572+
const encodedTransaction = this.build();
573+
const transaction = decodeTransactionUnsafe(hexToBin(encodedTransaction));
567574

568575
const libauthSourceOutputs = generateLibauthSourceOutputs(this.inputs);
569576
const sourceOutputs: WcSourceOutput[] = libauthSourceOutputs.map((sourceOutput, index) => {
@@ -572,7 +579,41 @@ export class TransactionBuilder {
572579
...transaction.inputs[index],
573580
...getWcContractInfo(this.inputs[index]),
574581
};
575-
});
576-
return { ...options, transaction, sourceOutputs };
577-
}
578-
}
582+
});
583+
return { ...options, transaction, sourceOutputs };
584+
}
585+
586+
/**
587+
* Build the transaction and format it as a WizardConnect transaction request.
588+
*
589+
* WizardConnect uses the standard BCH WalletConnect transaction object plus HD path metadata for
590+
* each placeholder P2PKH input that the wallet must sign.
591+
*
592+
* @param options - Optional WalletConnect options such as `broadcast` and `userPrompt`.
593+
* @returns A WizardConnect transaction object ready to be sent to a WizardConnect wallet.
594+
* @throws If the transaction cannot be built, or if a placeholder input is missing HD path metadata.
595+
*/
596+
generateWizardConnectTransactionObject(options?: WcTransactionOptions): WizardConnectTransactionObject {
597+
const transaction = this.generateWcTransactionObject(options);
598+
const inputPaths = this.generateWizardConnectInputPaths();
599+
600+
return { transaction, inputPaths };
601+
}
602+
603+
private generateWizardConnectInputPaths(): WizardConnectInputPath[] {
604+
const inputPaths: WizardConnectInputPath[] = [];
605+
606+
this.inputs.forEach((input, inputIndex) => {
607+
if (!isPlaceholderUnlocker(input.unlocker)) return;
608+
609+
const hdPath = input.unlocker.hdPath;
610+
if (!hdPath) {
611+
throw new Error(`Placeholder P2PKH input ${inputIndex} is missing WizardConnect HD path metadata`);
612+
}
613+
614+
inputPaths.push([inputIndex, hdPath.name, hdPath.addressIndex]);
615+
});
616+
617+
return inputPaths;
618+
}
619+
}

packages/cashscript/src/interfaces.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,24 @@ export interface P2PKHUnlocker extends Unlocker {
5656

5757
export type StandardUnlocker = ContractUnlocker | P2PKHUnlocker;
5858

59-
export type PlaceholderP2PKHUnlocker = Unlocker & { placeholder: true };
59+
export interface PlaceholderHdPath {
60+
name: string;
61+
addressIndex: number;
62+
}
63+
64+
export interface PlaceholderP2PKHUnlockerOptions {
65+
hdPath?: PlaceholderHdPath;
66+
}
67+
68+
export interface PlaceholderP2PKHUnlockerConfig extends PlaceholderP2PKHUnlockerOptions {
69+
address: string;
70+
}
71+
72+
export interface PlaceholderP2PKHUnlocker extends Unlocker {
73+
placeholder: true;
74+
address: string;
75+
hdPath?: PlaceholderHdPath;
76+
}
6077

6178
export type ContractFunctionUnlocker = (...args: FunctionArgument[]) => ContractUnlocker;
6279

packages/cashscript/src/walletconnect-utils.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { type LibauthOutput, isContractUnlocker, type PlaceholderP2PKHUnlocker, type UnlockableUtxo } from './interfaces.js';
1+
import {
2+
type LibauthOutput,
3+
isContractUnlocker,
4+
type PlaceholderP2PKHUnlockerConfig,
5+
type PlaceholderP2PKHUnlocker,
6+
type PlaceholderP2PKHUnlockerOptions,
7+
type UnlockableUtxo,
8+
} from './interfaces.js';
29
import { type AbiFunction, type Artifact } from '@cashscript/utils';
310
import { cashAddressToLockingBytecode, hexToBin, type Input, type TransactionCommon } from '@bitauth/libauth';
411

@@ -17,6 +24,13 @@ export interface WcTransactionObject {
1724
userPrompt?: string;
1825
}
1926

27+
export type WizardConnectInputPath = [inputIndex: number, pathName: string, addressIndex: number];
28+
29+
export interface WizardConnectTransactionObject {
30+
transaction: WcTransactionObject;
31+
inputPaths: WizardConnectInputPath[];
32+
}
33+
2034
export type WcSourceOutput = Input & LibauthOutput & WcContractInfo;
2135

2236
export interface WcContractInfo {
@@ -69,11 +83,25 @@ export const placeholderPublicKey = (): Uint8Array => Uint8Array.from(Array(33))
6983
* when building a transaction object for WalletConnect signing where the final signing is
7084
* performed by the connected wallet.
7185
*
72-
* @param userAddress - The user's CashAddress that will eventually sign the input.
86+
* @param userAddress - The user's CashAddress that will eventually sign the input, or an object
87+
* containing the address and signing metadata.
88+
* @param options - Optional signing metadata, such as HD path information for WizardConnect.
7389
* @returns A placeholder unlocker that can be passed to `TransactionBuilder.addInput`.
7490
* @throws If `userAddress` is not a valid CashAddress.
7591
*/
76-
export const placeholderP2PKHUnlocker = (userAddress: string): PlaceholderP2PKHUnlocker => {
92+
export function placeholderP2PKHUnlocker(
93+
userAddress: string,
94+
options?: PlaceholderP2PKHUnlockerOptions,
95+
): PlaceholderP2PKHUnlocker;
96+
export function placeholderP2PKHUnlocker(options: PlaceholderP2PKHUnlockerConfig): PlaceholderP2PKHUnlocker;
97+
export function placeholderP2PKHUnlocker(
98+
userAddressOrOptions: string | PlaceholderP2PKHUnlockerConfig,
99+
options: PlaceholderP2PKHUnlockerOptions = {},
100+
): PlaceholderP2PKHUnlocker {
101+
const userAddress = typeof userAddressOrOptions === 'string'
102+
? userAddressOrOptions
103+
: userAddressOrOptions.address;
104+
const unlockerOptions = typeof userAddressOrOptions === 'string' ? options : userAddressOrOptions;
77105
const decodeAddressResult = cashAddressToLockingBytecode(userAddress);
78106

79107
if (typeof decodeAddressResult === 'string') {
@@ -85,5 +113,7 @@ export const placeholderP2PKHUnlocker = (userAddress: string): PlaceholderP2PKHU
85113
generateLockingBytecode: () => lockingBytecode,
86114
generateUnlockingBytecode: () => Uint8Array.from(Array(0)),
87115
placeholder: true,
116+
address: userAddress,
117+
...unlockerOptions,
88118
};
89-
};
119+
}

packages/cashscript/test/TransactionBuilder.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,56 @@ describe('Transaction Builder', () => {
250250
});
251251
});
252252

253+
describe('test TransactionBuilder.generateWizardConnectTransactionObject', () => {
254+
it('should generate input paths for placeholder P2PKH inputs with HD path metadata', async () => {
255+
const p2pkhUtxos = (await p2pkhInstance.getUtxos()).filter(isNonTokenUtxo).sort(utxoComparator).reverse();
256+
const contractUtxo = p2pkhUtxos[0];
257+
const bobUtxos = await provider.getUtxos(bobAddress);
258+
const carolUtxos = await provider.getUtxos(carolAddress);
259+
260+
const placeholderPubKey = placeholderPublicKey();
261+
const placeholderSig = placeholderSignature();
262+
263+
const transactionBuilder = new TransactionBuilder({ provider })
264+
.addInput(contractUtxo, p2pkhInstance.unlock.spend(placeholderPubKey, placeholderSig))
265+
.addInput(bobUtxos[0], placeholderP2PKHUnlocker(bobAddress, {
266+
hdPath: { name: 'receive', addressIndex: 5 },
267+
}))
268+
.addInput(carolUtxos[0], placeholderP2PKHUnlocker({
269+
address: carolAddress,
270+
hdPath: { name: 'change', addressIndex: 2 },
271+
}))
272+
.addOutput({ to: bobAddress, amount: 100_000n });
273+
274+
const wizardConnectTransactionObj = transactionBuilder.generateWizardConnectTransactionObject({
275+
broadcast: false,
276+
userPrompt: 'Example WizardConnect transaction',
277+
});
278+
279+
expect(wizardConnectTransactionObj.transaction).toMatchObject({
280+
broadcast: false,
281+
userPrompt: 'Example WizardConnect transaction',
282+
});
283+
expect(wizardConnectTransactionObj.transaction.sourceOutputs).toHaveLength(3);
284+
expect(wizardConnectTransactionObj.inputPaths).toEqual([
285+
[1, 'receive', 5],
286+
[2, 'change', 2],
287+
]);
288+
});
289+
290+
it('should fail when a placeholder P2PKH input is missing HD path metadata', async () => {
291+
const bobUtxos = await provider.getUtxos(bobAddress);
292+
293+
const transactionBuilder = new TransactionBuilder({ provider })
294+
.addInput(bobUtxos[0], placeholderP2PKHUnlocker(bobAddress))
295+
.addOutput({ to: bobAddress, amount: 100_000n });
296+
297+
expect(() => transactionBuilder.generateWizardConnectTransactionObject()).toThrow(
298+
'Placeholder P2PKH input 0 is missing WizardConnect HD path metadata',
299+
);
300+
});
301+
});
302+
253303
it('should not fail when validly spending from only P2PKH inputs', async () => {
254304
const aliceUtxos = (await provider.getUtxos(aliceAddress)).filter(isNonTokenUtxo);
255305
const sigTemplate = new SignatureTemplate(alicePriv);

0 commit comments

Comments
 (0)