Skip to content

Commit 73a9cb1

Browse files
committed
Fix bug with missing unlockingBytecode in scenario transaction inputs
- Update Unlocker typings for better typescript usage - Update generateTemplateScenarioBytecode to add unlockingBytecode data for P2SH inputs - Update 1 fixture, rest still needs updating - Small refactors to advanced libauth template generation
1 parent 8f0a808 commit 73a9cb1

File tree

5 files changed

+165
-76
lines changed

5 files changed

+165
-76
lines changed

packages/cashscript/src/LibauthTemplate.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { Transaction } from './Transaction.js';
4040
import { EncodedConstructorArgument, EncodedFunctionArgument } from './Argument.js';
4141
import { addressToLockScript, extendedStringify, zip } from './utils.js';
4242
import { Contract } from './Contract.js';
43+
import { generateUnlockingScriptParams } from './advanced/LibauthTemplate.js';
4344

4445
interface BuildTemplateOptions {
4546
transaction: Transaction;
@@ -257,6 +258,7 @@ const generateTemplateScenarios = (
257258
data: {
258259
// encode values for the variables defined above in `entities` property
259260
bytecode: {
261+
...generateTemplateScenarioParametersFunctionIndex(abiFunction, artifact.abi),
260262
...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs),
261263
...generateTemplateScenarioParametersValues(artifact.constructorInputs, encodedConstructorArgs),
262264
},
@@ -271,11 +273,6 @@ const generateTemplateScenarios = (
271273
},
272274
};
273275

274-
if (artifact.abi.length > 1) {
275-
const functionIndex = artifact.abi.findIndex((func) => func.name === transaction.abiFunction.name);
276-
scenarios![artifact.contractName + '_evaluate'].data!.bytecode!.function_index = functionIndex.toString();
277-
}
278-
279276
return scenarios;
280277
};
281278

@@ -286,14 +283,14 @@ const generateTemplateScenarioTransaction = (
286283
): WalletTemplateScenario['transaction'] => {
287284
const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input));
288285

289-
const inputs = libauthTransaction.inputs.map((input, index) => {
290-
const csInput = csTransaction.inputs[index] as Utxo;
286+
const inputs = libauthTransaction.inputs.map((input, inputIndex) => {
287+
const csInput = csTransaction.inputs[inputIndex] as Utxo;
291288

292289
return {
293290
outpointIndex: input.outpointIndex,
294291
outpointTransactionHash: binToHex(input.outpointTransactionHash),
295292
sequenceNumber: input.sequenceNumber,
296-
unlockingBytecode: generateTemplateScenarioBytecode(csInput, `p2pkh_placeholder_unlock_${index}`, `placeholder_key_${index}`, index === slotIndex),
293+
unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', inputIndex === slotIndex),
297294
} as WalletTemplateScenarioInput;
298295
});
299296

@@ -337,14 +334,15 @@ export const generateTemplateScenarioTransactionOutputLockingBytecode = (
337334
* The slotIndex tracks which input is the contract input vs P2PKH inputs
338335
* to properly generate the locking scripts.
339336
*/
337+
// TODO: This looks like it needs some refactor to work with the new stuff
340338
const generateTemplateScenarioSourceOutputs = (
341339
csTransaction: Transaction,
342340
): Array<WalletTemplateScenarioOutput<true>> => {
343341
const slotIndex = csTransaction.inputs.findIndex((input) => !isUtxoP2PKH(input));
344342

345-
return csTransaction.inputs.map((input, index) => {
343+
return csTransaction.inputs.map((input, inputIndex) => {
346344
return {
347-
lockingBytecode: generateTemplateScenarioBytecode(input, `p2pkh_placeholder_lock_${index}`, `placeholder_key_${index}`, index === slotIndex),
345+
lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex),
348346
valueSatoshis: Number(input.satoshis),
349347
token: serialiseTokenDetails(input.token),
350348
};
@@ -353,8 +351,13 @@ const generateTemplateScenarioSourceOutputs = (
353351

354352
// Used for generating the locking / unlocking bytecode for source outputs and inputs
355353
export const generateTemplateScenarioBytecode = (
356-
input: Utxo, p2pkhScriptName: string, placeholderKeyName: string, insertSlot?: boolean,
354+
input: Utxo, inputIndex: number, p2pkhScriptNameTemplate: string, insertSlot?: boolean,
357355
): WalletTemplateScenarioBytecode | ['slot'] => {
356+
if (insertSlot) return ['slot'];
357+
358+
const p2pkhScriptName = `${p2pkhScriptNameTemplate}_${inputIndex}`;
359+
const placeholderKeyName = `placeholder_key_${inputIndex}`;
360+
358361
// This is for P2PKH inputs in the old transaction builder (TODO: remove when we remove old transaction builder)
359362
if (isUtxoP2PKH(input)) {
360363
return {
@@ -369,23 +372,13 @@ export const generateTemplateScenarioBytecode = (
369372
};
370373
}
371374

372-
// This is for P2PKH inputs in the new transaction builder
373-
if (isUnlockableUtxo(input) && input.unlocker.template) {
374-
return {
375-
script: p2pkhScriptName,
376-
overrides: {
377-
keys: {
378-
privateKeys: {
379-
[placeholderKeyName]: binToHex(input.unlocker.template.privateKey),
380-
},
381-
},
382-
},
383-
};
375+
if (isUnlockableUtxo(input)) {
376+
return generateUnlockingScriptParams(input, p2pkhScriptNameTemplate, inputIndex);
384377
}
385378

386379
// 'slot' means that we are currently evaluating this specific input,
387380
// {} means that it is the same script type, but not being evaluated
388-
return insertSlot ? ['slot'] : {};
381+
return {};
389382
};
390383

391384
export const generateTemplateScenarioParametersValues = (
@@ -406,6 +399,17 @@ export const generateTemplateScenarioParametersValues = (
406399
return Object.fromEntries(entries);
407400
};
408401

402+
export const generateTemplateScenarioParametersFunctionIndex = (
403+
abiFunction: AbiFunction,
404+
abi: readonly AbiFunction[],
405+
): Record<string, string> => {
406+
const functionIndex = abi.length > 1
407+
? abi.findIndex((func) => func.name === abiFunction.name)
408+
: undefined;
409+
410+
return functionIndex !== undefined ? { function_index: functionIndex.toString() } : {};
411+
};
412+
409413
export const addHexPrefixExceptEmpty = (value: string): string => {
410414
return value.length > 0 ? `0x${value}` : '';
411415
};

packages/cashscript/src/advanced/LibauthTemplate.ts

Lines changed: 88 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { Contract } from '../Contract.js';
2222
import { DebugResults, debugTemplate } from '../debugging.js';
2323
import {
2424
AddressType,
25+
ContractUnlocker,
2526
UnlockableUtxo,
2627
Utxo,
2728
} from '../interfaces.js';
@@ -31,6 +32,7 @@ import {
3132
formatParametersForDebugging,
3233
generateTemplateScenarioBytecode,
3334
generateTemplateScenarioKeys,
35+
generateTemplateScenarioParametersFunctionIndex,
3436
generateTemplateScenarioParametersValues,
3537
generateTemplateScenarioTransactionOutputLockingBytecode,
3638
getHashTypeName,
@@ -85,6 +87,35 @@ export const generateTemplateEntitiesP2SH = (
8587
encodedFunctionArgs: EncodedFunctionArgument[],
8688
inputIndex: number,
8789
): WalletTemplate['entities'] => {
90+
const entities = {
91+
[artifact.contractName + '_input' + inputIndex + '_parameters']: {
92+
description: 'Contract creation and function parameters',
93+
name: `${artifact.contractName} (input #${inputIndex})`,
94+
scripts: [
95+
artifact.contractName + '_lock',
96+
artifact.contractName + '_' + abiFunction.name + '_input' + inputIndex + '_unlock',
97+
],
98+
variables: createWalletTemplateVariables(artifact, abiFunction, encodedFunctionArgs),
99+
},
100+
};
101+
102+
// function_index is a special variable that indicates the function to execute
103+
if (artifact.abi.length > 1) {
104+
entities[artifact.contractName + '_input' + inputIndex + '_parameters'].variables.function_index = {
105+
description: 'Script function index to execute',
106+
name: 'function_index',
107+
type: 'WalletData',
108+
};
109+
}
110+
111+
return entities;
112+
};
113+
114+
const createWalletTemplateVariables = (
115+
artifact: Artifact,
116+
abiFunction: AbiFunction,
117+
encodedFunctionArgs: EncodedFunctionArgument[],
118+
): Record<string, WalletTemplateVariable> => {
88119
const functionParameters = Object.fromEntries<WalletTemplateVariable>(
89120
abiFunction.inputs.map((input, index) => ([
90121
input.name,
@@ -107,31 +138,7 @@ export const generateTemplateEntitiesP2SH = (
107138
])),
108139
);
109140

110-
const entities = {
111-
[artifact.contractName + '_input' + inputIndex + '_parameters']: {
112-
description: 'Contract creation and function parameters',
113-
name: `${artifact.contractName} (input #${inputIndex})`,
114-
scripts: [
115-
artifact.contractName + '_lock',
116-
artifact.contractName + '_' + abiFunction.name + '_input' + inputIndex + '_unlock',
117-
],
118-
variables: {
119-
...functionParameters,
120-
...constructorParameters,
121-
},
122-
},
123-
};
124-
125-
// function_index is a special variable that indicates the function to execute
126-
if (artifact.abi.length > 1) {
127-
entities[artifact.contractName + '_input' + inputIndex + '_parameters'].variables.function_index = {
128-
description: 'Script function index to execute',
129-
name: 'function_index',
130-
type: 'WalletData',
131-
};
132-
}
133-
134-
return entities;
141+
return { ...functionParameters, ...constructorParameters };
135142
};
136143

137144
/**
@@ -303,14 +310,14 @@ const generateTemplateScenarioTransaction = (
303310
csTransaction: Transaction,
304311
slotIndex: number,
305312
): WalletTemplateScenario['transaction'] => {
306-
const inputs = libauthTransaction.inputs.map((input, index) => {
307-
const csInput = csTransaction.inputs[index] as Utxo;
313+
const inputs = libauthTransaction.inputs.map((input, inputIndex) => {
314+
const csInput = csTransaction.inputs[inputIndex] as Utxo;
308315

309316
return {
310317
outpointIndex: input.outpointIndex,
311318
outpointTransactionHash: binToHex(input.outpointTransactionHash),
312319
sequenceNumber: input.sequenceNumber,
313-
unlockingBytecode: generateTemplateScenarioBytecode(csInput, `p2pkh_placeholder_unlock_${index}`, `placeholder_key_${index}`, slotIndex === index),
320+
unlockingBytecode: generateTemplateScenarioBytecode(csInput, inputIndex, 'p2pkh_placeholder_unlock', slotIndex === inputIndex),
314321
} as WalletTemplateScenarioInput;
315322
});
316323

@@ -335,10 +342,9 @@ const generateTemplateScenarioSourceOutputs = (
335342
csTransaction: Transaction,
336343
slotIndex: number,
337344
): Array<WalletTemplateScenarioOutput<true>> => {
338-
339-
return csTransaction.inputs.map((input, index) => {
345+
return csTransaction.inputs.map((input, inputIndex) => {
340346
return {
341-
lockingBytecode: generateTemplateScenarioBytecode(input, `p2pkh_placeholder_lock_${index}`, `placeholder_key_${index}`, index === slotIndex),
347+
lockingBytecode: generateTemplateScenarioBytecode(input, inputIndex, 'p2pkh_placeholder_lock', inputIndex === slotIndex),
342348
valueSatoshis: Number(input.satoshis),
343349
token: serialiseTokenDetails(input.token),
344350
};
@@ -396,7 +402,7 @@ export const getLibauthTemplates = (
396402

397403
for (const [inputIndex, input] of txn.inputs.entries()) {
398404
// If template exists on the input, it indicates this is a P2PKH (Pay to Public Key Hash) input
399-
if (input.unlocker?.template) {
405+
if ('template' in input.unlocker) {
400406
// @ts-ignore TODO: Remove UtxoP2PKH type and only use UnlockableUtxo in Libaith Template generation
401407
input.template = input.unlocker?.template; // Added to support P2PKH inputs in buildTemplate
402408
Object.assign(p2pkhEntities, generateTemplateEntitiesP2PKH(inputIndex));
@@ -406,7 +412,7 @@ export const getLibauthTemplates = (
406412
}
407413

408414
// If contract exists on the input, it indicates this is a contract input
409-
if (input.unlocker?.contract) {
415+
if ('contract' in input.unlocker) {
410416
const contract = input.unlocker?.contract;
411417
const abiFunction = input.unlocker?.abiFunction;
412418

@@ -454,9 +460,8 @@ export const getLibauthTemplates = (
454460
const lockScriptName = Object.keys(script).find(scriptName => scriptName.includes('_lock'));
455461
if (lockScriptName) {
456462
// Generate bytecodes for this contract input
457-
const csInput = csTransaction.inputs[inputIndex];
458463
const unlockingBytecode = binToHex(libauthTransaction.inputs[inputIndex].unlockingBytecode);
459-
const lockingScriptParams = generateLockingScriptParams(csInput.unlocker!.contract!, csInput, lockScriptName);
464+
const lockingScriptParams = generateLockingScriptParams(input.unlocker.contract, input, lockScriptName);
460465

461466
// Assign a name to the unlocking bytecode so later it can be used to replace the bytecode/slot in scenarios
462467
unlockingBytecodeToLockingBytecodeParams[unlockingBytecode] = lockingScriptParams;
@@ -525,7 +530,7 @@ export const getLibauthTemplates = (
525530

526531
export const debugLibauthTemplate = (template: WalletTemplate, transaction: TransactionBuilder): DebugResults => {
527532
const allArtifacts = transaction.inputs
528-
.map(input => input.unlocker?.contract)
533+
.map(input => 'contract' in input.unlocker ? input.unlocker.contract : undefined)
529534
.filter((contract): contract is Contract => !!contract)
530535
.map(contract => contract.artifact);
531536

@@ -537,14 +542,19 @@ const generateLockingScriptParams = (
537542
csInput: UnlockableUtxo,
538543
lockScriptName: string,
539544
): WalletTemplateScenarioBytecode => {
540-
if (!csInput.unlocker?.contract) return {
541-
script: lockScriptName,
542-
};
545+
if (('template' in csInput.unlocker)) {
546+
return {
547+
script: lockScriptName,
548+
};
549+
}
543550

544551
const constructorParamsEntries = contract.artifact.constructorInputs
545552
.map(({ name }, index) => [
546553
name,
547-
addHexPrefixExceptEmpty(binToHex(csInput.unlocker!.contract!.encodedConstructorArgs[index])),
554+
// TODO: For some reason, typescript forgets that the unlocker is a ContractUnlocker
555+
addHexPrefixExceptEmpty(
556+
binToHex((csInput.unlocker as ContractUnlocker).contract.encodedConstructorArgs[index]),
557+
),
548558
]);
549559

550560
const constructorParams = Object.fromEntries(constructorParamsEntries);
@@ -556,3 +566,41 @@ const generateLockingScriptParams = (
556566
},
557567
};
558568
};
569+
570+
export const generateUnlockingScriptParams = (
571+
csInput: UnlockableUtxo,
572+
p2pkhScriptNameTemplate: string,
573+
inputIndex: number,
574+
): WalletTemplateScenarioBytecode => {
575+
if (('template' in csInput.unlocker)) {
576+
return {
577+
script: `${p2pkhScriptNameTemplate}_${inputIndex}`,
578+
overrides: {
579+
keys: {
580+
privateKeys: {
581+
[`placeholder_key_${inputIndex}`]: binToHex(csInput.unlocker.template.privateKey),
582+
},
583+
},
584+
},
585+
};
586+
}
587+
588+
const abiFunction = csInput.unlocker.abiFunction;
589+
const contract = csInput.unlocker.contract;
590+
const encodedFunctionArgs = encodeFunctionArguments(abiFunction, csInput.unlocker.params);
591+
592+
return {
593+
script: `${csInput.unlocker.contract.name}_${abiFunction.name}_input${inputIndex}_unlock`,
594+
overrides: {
595+
// encode values for the variables defined above in `entities` property
596+
bytecode: {
597+
...generateTemplateScenarioParametersFunctionIndex(abiFunction, contract.artifact.abi),
598+
...generateTemplateScenarioParametersValues(abiFunction.inputs, encodedFunctionArgs),
599+
...generateTemplateScenarioParametersValues(contract.artifact.constructorInputs, contract.encodedConstructorArgs),
600+
},
601+
keys: {
602+
privateKeys: generateTemplateScenarioKeys(abiFunction.inputs, encodedFunctionArgs),
603+
},
604+
},
605+
};
606+
};

packages/cashscript/src/interfaces.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,23 @@ export interface GenerateUnlockingBytecodeOptions {
3232
}
3333

3434
// TODO: Change this type when we understand the requirements better
35-
export interface Unlocker {
35+
export interface BaseUnlocker {
3636
generateLockingBytecode: () => Uint8Array;
3737
generateUnlockingBytecode: (options: GenerateUnlockingBytecodeOptions) => Uint8Array;
38-
contract?: Contract;
39-
params?: FunctionArgument[];
40-
abiFunction?: AbiFunction;
41-
template?: SignatureTemplate;
4238
}
4339

40+
export interface ContractUnlocker extends BaseUnlocker {
41+
contract: Contract;
42+
params: FunctionArgument[];
43+
abiFunction: AbiFunction;
44+
}
45+
46+
export interface P2PKHUnlocker extends BaseUnlocker {
47+
template: SignatureTemplate;
48+
}
49+
50+
export type Unlocker = ContractUnlocker | P2PKHUnlocker;
51+
4452
export interface UtxoP2PKH extends Utxo {
4553
template: SignatureTemplate;
4654
}

0 commit comments

Comments
 (0)