From 59560250dca6bcac70b44689dc0ba4f4f68a7f68 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Sun, 22 Jun 2025 09:52:19 +0200 Subject: [PATCH 1/4] add wc-utils --- packages/cashscript/src/TransactionBuilder.ts | 11 +--- packages/cashscript/src/index.ts | 1 + packages/cashscript/src/utils.ts | 14 +++++ packages/cashscript/src/wc-utils.ts | 54 +++++++++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 packages/cashscript/src/wc-utils.ts diff --git a/packages/cashscript/src/TransactionBuilder.ts b/packages/cashscript/src/TransactionBuilder.ts index 497287c5..f3f4bbd9 100644 --- a/packages/cashscript/src/TransactionBuilder.ts +++ b/packages/cashscript/src/TransactionBuilder.ts @@ -22,6 +22,7 @@ import { NetworkProvider } from './network/index.js'; import { cashScriptOutputToLibauthOutput, createOpReturnOutput, + generateLibauthSourceOutputs, validateInput, validateOutput, } from './utils.js'; @@ -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 }) diff --git a/packages/cashscript/src/index.ts b/packages/cashscript/src/index.ts index 53c364bb..bc733cdc 100644 --- a/packages/cashscript/src/index.ts +++ b/packages/cashscript/src/index.ts @@ -21,3 +21,4 @@ export { MockNetworkProvider, } from './network/index.js'; export { randomUtxo, randomToken, randomNFT } from './utils.js'; +export * from './wc-utils.js'; diff --git a/packages/cashscript/src/utils.ts b/packages/cashscript/src/utils.ts index 26699d88..0771083c 100644 --- a/packages/cashscript/src/utils.ts +++ b/packages/cashscript/src/utils.ts @@ -32,6 +32,7 @@ import { LibauthOutput, TokenDetails, AddressType, + UnlockableUtxo, } from './interfaces.js'; import { VERSION_SIZE, LOCKTIME_SIZE } from './constants.js'; import { @@ -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); diff --git a/packages/cashscript/src/wc-utils.ts b/packages/cashscript/src/wc-utils.ts new file mode 100644 index 00000000..577c09bd --- /dev/null +++ b/packages/cashscript/src/wc-utils.ts @@ -0,0 +1,54 @@ +import { isStandardUnlockableUtxo } from './index.js'; +import type { UnlockableUtxo, StandardUnlockableUtxo, LibauthOutput } from './interfaces.js'; +import { generateLibauthSourceOutputs } from './utils.js'; +import { type AbiFunction, type Artifact, scriptToBytecode } from '@cashscript/utils'; +import type { Input, TransactionCommon } from '@bitauth/libauth'; + +// Wallet Connect interfaces according to the spec +// see https://github.com/mainnet-pat/wc2-bch-bcr + +export interface WcContractInfo { + contract?: { + abiFunction: AbiFunction; + redeemScript: Uint8Array; + artifact: Partial; + } +} + +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 {}; + 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 generateWcSourceOutputs( + inputs: UnlockableUtxo[], decodedTransaction:TransactionCommon, +): WcSourceOutputs { + 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) => { + return { + ...sourceOutput, + ...decodedTransaction.inputs[index], + ...getWcContractInfo(inputs[index]), + }; + }); + return wcSourceOutputs; +} From 9c7d9379f78f969243502297bc6347387a11acc3 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Sun, 22 Jun 2025 10:08:35 +0200 Subject: [PATCH 2/4] add to walletconnect docs --- website/docs/guides/walletconnect.md | 32 ++++++++++++---------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/website/docs/guides/walletconnect.md b/website/docs/guides/walletconnect.md index 7cca98a9..33339c28 100644 --- a/website/docs/guides/walletconnect.md +++ b/website/docs/guides/walletconnect.md @@ -27,7 +27,13 @@ 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: +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. + +The `sourceOutputs` value can be easily generated with the CashScript `generateWcSourceOutputs` helperfunction. + +## User-Wallet placeholders + +Important to understand is that the standard uses placeholder values which the user wallet then needs to fill in. For the user inputs to sign the placeholders is the following: >To signal that the wallet needs to sign an input, the app sets the corresponding input's `unlockingBytecode` to empty Uint8Array. @@ -46,7 +52,7 @@ Below we'll give 2 example, the first example using spending a user-input and in 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, generateWcSourceOutputs, type Unlocker } from "cashscript"; import { hexToBin, decodeTransaction } from "@bitauth/libauth"; async function proposeWcTransaction(){ @@ -68,18 +74,13 @@ async function proposeWcTransaction(){ 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] } - }) + // construct WcSourceOutputs from transaction input using a CashScript helper function + const wcSourceOutputs = generateWcSourceOutputs(transactionBuilder.inputs) // wcTransactionObj to pass to signTransaction endpoint const wcTransactionObj = { transaction: decodedTransaction, - sourceOutputs: listSourceOutputs, + sourceOutputs: wcSourceOutputs, broadcast: true, userPrompt: "Create HODL Contract" }; @@ -96,7 +97,7 @@ 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, generateWcSourceOutputs } from "cashscript"; import { hexToBin, decodeTransaction } from "@bitauth/libauth"; async function unlockHodlVault(){ @@ -115,13 +116,8 @@ async function unlockHodlVault(){ 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] } - }) + // construct WcSourceOutputs from transaction input using a CashScript helper function + const wcSourceOutputs = generateWcSourceOutputs(transactionBuilder.inputs) const wcTransactionObj = { transaction: decodedTransaction, From 46c410a8d3cc90dcaef458069cabbe3c8a5d2d99 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Sun, 22 Jun 2025 11:17:35 +0200 Subject: [PATCH 3/4] add PlaceholderTemplate --- .../cashscript/src/PlaceholderTemplate.ts | 42 +++++++++++++++++++ website/docs/guides/walletconnect.md | 34 +++++---------- 2 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 packages/cashscript/src/PlaceholderTemplate.ts diff --git a/packages/cashscript/src/PlaceholderTemplate.ts b/packages/cashscript/src/PlaceholderTemplate.ts new file mode 100644 index 00000000..7cb6b27f --- /dev/null +++ b/packages/cashscript/src/PlaceholderTemplate.ts @@ -0,0 +1,42 @@ +import { cashAddressToLockingBytecode } from '@bitauth/libauth'; +import { P2PKHUnlocker } from './interfaces.js'; +import SignatureTemplate from './SignatureTemplate.js'; + +export default class PlaceholderTemplate { + public privateKey: Uint8Array; + private lockingBytecode: Uint8Array; + + constructor( + address?: string, + ) { + if (address) { + const decodeAddressResult = cashAddressToLockingBytecode(address); + if (typeof decodeAddressResult === 'string') { + throw new Error(`Invalid address: ${decodeAddressResult}`); + } + this.lockingBytecode = decodeAddressResult.bytecode; + } + } + + // TODO: should the arguments 'generateSignature' match? + // do the other methods (getHashType, getSignatureAlgorithm) need to be implemented? + + // Currently in the walletconnect spec, only schnorr (65-byte) signatures are supported + generateSignature(): Uint8Array { + return Uint8Array.from(Array(65)); + } + + getPublicKey(): Uint8Array { + return Uint8Array.from(Array(33)); + } + + unlockP2PKH(): P2PKHUnlocker { + const lockingBytecode = this.lockingBytecode ?? Uint8Array.from(Array(0)); + return { + generateLockingBytecode: () => lockingBytecode, + generateUnlockingBytecode: () => Uint8Array.from(Array(0)), + // TODO: pass 'this' when the types allows for it + template: new SignatureTemplate(Uint8Array.from(Array(0))), + }; + } +} \ No newline at end of file diff --git a/website/docs/guides/walletconnect.md b/website/docs/guides/walletconnect.md index 33339c28..3d57b48f 100644 --- a/website/docs/guides/walletconnect.md +++ b/website/docs/guides/walletconnect.md @@ -27,20 +27,10 @@ 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. +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. The `sourceOutputs` value can be easily generated with the CashScript `generateWcSourceOutputs` helperfunction. -## User-Wallet placeholders - -Important to understand is that the standard uses placeholder values which the user wallet then needs to fill in. For the user inputs to sign the placeholders is the following: - ->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`. @@ -52,19 +42,16 @@ Below we'll give 2 example, the first example using spending a user-input and in 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, type Unlocker } from "cashscript"; +import { TransactionBuilder, generateWcSourceOutputs, placeholderTemplate } 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){ + // create a placeholderTemplate to generate placeholder unlocker + const placeholderTemplate = new PlaceholderTemplate(userAddress) // use the CashScript SDK to build a transaction const transactionBuilder = new TransactionBuilder({provider: store.provider}) - transactionBuilder.addInputs(userInputUtxos, placeholderUnlocker) + transactionBuilder.addInputs(userInputUtxos, placeholderTemplate.unlockP2PKH()) transactionBuilder.addOpReturnOutput(opReturnData) transactionBuilder.addOutput(contractOutput) if(changeAmount > 550n) transactionBuilder.addOutput(changeOutput) @@ -97,13 +84,14 @@ 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 { TransactionBuilder, generateWcSourceOutputs } from "cashscript"; +import { TransactionBuilder, generateWcSourceOutputs, PlaceholderTemplate } 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)); + // use placeholderTemplate to create placeholder args for unlocker + const placeholderTemplate = new PlaceholderTemplate() + const placeholderSig = placeholderTemplate.generateSignature() + const placeholderPubKey = placeholderTemplate.getPublicKey() const transactionBuilder = new TransactionBuilder({provider: store.provider}) From cd5ffab960f2c207a1102e96cd1a4a25e2584592 Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Thu, 26 Jun 2025 12:01:51 +0200 Subject: [PATCH 4/4] WIP --- packages/cashscript/src/Argument.ts | 5 +- packages/cashscript/src/Contract.ts | 2 + .../cashscript/src/PlaceholderTemplate.ts | 44 ++++++++++------ packages/cashscript/src/wc-utils.ts | 50 +++++++++++++++---- website/docs/guides/walletconnect.md | 42 +++++++--------- 5 files changed, 89 insertions(+), 54 deletions(-) diff --git a/packages/cashscript/src/Argument.ts b/packages/cashscript/src/Argument.ts index 915fe84d..65d515d9 100644 --- a/packages/cashscript/src/Argument.ts +++ b/packages/cashscript/src/Argument.ts @@ -11,12 +11,13 @@ import { } from '@cashscript/utils'; import { TypeError } from './Errors.js'; import SignatureTemplate from './SignatureTemplate.js'; +import PlaceholderTemplate from './PlaceholderTemplate.js'; export type ConstructorArgument = bigint | boolean | string | Uint8Array; -export type FunctionArgument = ConstructorArgument | SignatureTemplate; +export type FunctionArgument = ConstructorArgument | SignatureTemplate | PlaceholderTemplate; export type EncodedConstructorArgument = Uint8Array; -export type EncodedFunctionArgument = Uint8Array | SignatureTemplate; +export type EncodedFunctionArgument = Uint8Array | SignatureTemplate | PlaceholderTemplate; export type EncodeFunction = (arg: FunctionArgument, typeStr: string) => EncodedFunctionArgument; diff --git a/packages/cashscript/src/Contract.ts b/packages/cashscript/src/Contract.ts index e2baf3d2..43415a7f 100644 --- a/packages/cashscript/src/Contract.ts +++ b/packages/cashscript/src/Contract.ts @@ -25,6 +25,7 @@ import SignatureTemplate from './SignatureTemplate.js'; import { ElectrumNetworkProvider } from './network/index.js'; import { ParamsToTuple, AbiToFunctionMap } from './types/type-inference.js'; import semver from 'semver'; +import PlaceholderTemplate from './PlaceholderTemplate.js'; export class Contract< TArtifact extends Artifact = Artifact, @@ -161,6 +162,7 @@ export class Contract< { transaction, sourceOutputs, inputIndex }: GenerateUnlockingBytecodeOptions, ): Uint8Array => { const completeArgs = encodedArgs.map((arg) => { + if (arg instanceof PlaceholderTemplate) return arg.generateSignature(); if (!(arg instanceof SignatureTemplate)) return arg; // Generate transaction signature from SignatureTemplate diff --git a/packages/cashscript/src/PlaceholderTemplate.ts b/packages/cashscript/src/PlaceholderTemplate.ts index 7cb6b27f..4094e035 100644 --- a/packages/cashscript/src/PlaceholderTemplate.ts +++ b/packages/cashscript/src/PlaceholderTemplate.ts @@ -1,5 +1,5 @@ import { cashAddressToLockingBytecode } from '@bitauth/libauth'; -import { P2PKHUnlocker } from './interfaces.js'; +import { Unlocker } from './interfaces.js'; import SignatureTemplate from './SignatureTemplate.js'; export default class PlaceholderTemplate { @@ -7,18 +7,16 @@ export default class PlaceholderTemplate { private lockingBytecode: Uint8Array; constructor( - address?: string, + address: string, ) { - if (address) { - const decodeAddressResult = cashAddressToLockingBytecode(address); - if (typeof decodeAddressResult === 'string') { - throw new Error(`Invalid address: ${decodeAddressResult}`); - } - this.lockingBytecode = decodeAddressResult.bytecode; + const decodeAddressResult = cashAddressToLockingBytecode(address); + if (typeof decodeAddressResult === 'string') { + throw new Error(`Invalid address: ${decodeAddressResult}`); } + this.lockingBytecode = decodeAddressResult.bytecode; } - - // TODO: should the arguments 'generateSignature' match? + + // TODO: should the arguments 'generateSignature' match? // do the other methods (getHashType, getSignatureAlgorithm) need to be implemented? // Currently in the walletconnect spec, only schnorr (65-byte) signatures are supported @@ -30,13 +28,27 @@ export default class PlaceholderTemplate { return Uint8Array.from(Array(33)); } - unlockP2PKH(): P2PKHUnlocker { - const lockingBytecode = this.lockingBytecode ?? Uint8Array.from(Array(0)); + unlockP2PKH(): Unlocker { return { - generateLockingBytecode: () => lockingBytecode, + generateLockingBytecode: () => this.lockingBytecode, generateUnlockingBytecode: () => Uint8Array.from(Array(0)), - // TODO: pass 'this' when the types allows for it - template: new SignatureTemplate(Uint8Array.from(Array(0))), }; } -} \ No newline at end of file +} + +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)), + }; +}; diff --git a/packages/cashscript/src/wc-utils.ts b/packages/cashscript/src/wc-utils.ts index 577c09bd..c4917e4c 100644 --- a/packages/cashscript/src/wc-utils.ts +++ b/packages/cashscript/src/wc-utils.ts @@ -2,11 +2,20 @@ import { isStandardUnlockableUtxo } from './index.js'; import type { UnlockableUtxo, StandardUnlockableUtxo, LibauthOutput } 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 { 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; @@ -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 {}; @@ -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), @@ -36,19 +43,40 @@ function getWcContractInfo(input: StandardUnlockableUtxo): WcContractInfo | {} { return wcContractObj; } -export function generateWcSourceOutputs( - inputs: UnlockableUtxo[], decodedTransaction:TransactionCommon, -): WcSourceOutputs { +export function generateWcTransactionObject( + inputs: UnlockableUtxo[], encodedTransaction: string, +): WcTransactionObject { 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 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)), + }; +}; diff --git a/website/docs/guides/walletconnect.md b/website/docs/guides/walletconnect.md index 3d57b48f..b6691993 100644 --- a/website/docs/guides/walletconnect.md +++ b/website/docs/guides/walletconnect.md @@ -29,7 +29,7 @@ signTransaction: ( 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. -The `sourceOutputs` value can be easily generated with the CashScript `generateWcSourceOutputs` helperfunction. +The `sourceOutputs` value can be easily generated with the CashScript `generateWcSourceOutputs` helperfunction. ## Create wcTransactionObj @@ -46,28 +46,22 @@ import { TransactionBuilder, generateWcSourceOutputs, placeholderTemplate } from import { hexToBin, decodeTransaction } from "@bitauth/libauth"; async function proposeWcTransaction(userAddress: string){ - // create a placeholderTemplate to generate placeholder unlocker - const placeholderTemplate = new PlaceholderTemplate(userAddress) + // Create a placeholderTemplate + const unlocker = new PlaceholderTemplate(userAddress).unlockP2PKH() + const unlocker = getPlaceholderP2PKHUnlocker(userAddress) - // use the CashScript SDK to build a transaction + // Use the CashScript SDK to build a transaction const transactionBuilder = new TransactionBuilder({provider: store.provider}) - transactionBuilder.addInputs(userInputUtxos, placeholderTemplate.unlockP2PKH()) + transactionBuilder.addInputs(userInputUtxos, getPlaceholderP2PKHUnlocker(userAddress)) transactionBuilder.addOpReturnOutput(opReturnData) transactionBuilder.addOutput(contractOutput) if(changeAmount > 550n) transactionBuilder.addOutput(changeOutput) - const unsignedRawTransactionHex = await transactionBuilder.build(); + const rawTransactionHex = 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.inputs, rawTransactionHex), broadcast: true, userPrompt: "Create HODL Contract" }; @@ -87,7 +81,7 @@ Below is example code from the `unlockHodlVault` code of the 'Hodl Vault' dapp r import { TransactionBuilder, generateWcSourceOutputs, PlaceholderTemplate } from "cashscript"; import { hexToBin, decodeTransaction } from "@bitauth/libauth"; -async function unlockHodlVault(){ +async function unlockHodlVault() { // use placeholderTemplate to create placeholder args for unlocker const placeholderTemplate = new PlaceholderTemplate() const placeholderSig = placeholderTemplate.generateSignature() @@ -96,20 +90,18 @@ async function unlockHodlVault(){ const transactionBuilder = new TransactionBuilder({provider: store.provider}) transactionBuilder.setLocktime(store.currentBlockHeight) - transactionBuilder.addInputs(contractUtxos, hodlContract.unlock.spend(placeholderPubKey, placeholderSig)) + transactionBuilder.addInputs(contractUtxos, hodlContract.unlock.spend(getPlaceholderPubKey(), getPlaceholderTemplate())) transactionBuilder.addOutput(reclaimOutput) - const unsignedRawTransactionHex = transactionBuilder.build(); - - const decodedTransaction = decodeTransaction(hexToBin(unsignedRawTransactionHex)); - if(typeof decodedTransaction == "string") throw new Error("!decodedTransaction") + const rawTransactionHex = transactionBuilder.build(); - // construct WcSourceOutputs from transaction input using a CashScript helper function - const wcSourceOutputs = generateWcSourceOutputs(transactionBuilder.inputs) + // Generate the WalletConnect source outputs and raw transaction object + const { sourceOutputs, transaction } = generateWcTransactionObject(transactionBuilder.inputs, rawTransactionHex); + // Combine the source outputs and raw transaction object with custom 'broadcast' and 'userPrompt' properties const wcTransactionObj = { - transaction: decodedTransaction, - sourceOutputs: wcSourceOutputs, + transaction, + sourceOutputs, broadcast: true, userPrompt: "Reclaim HODL Value", };