Skip to content

Commit 3411125

Browse files
committed
Validate address network prefix and apply existing output validations to P2S as well
1 parent 9f8e65e commit 3411125

3 files changed

Lines changed: 55 additions & 17 deletions

File tree

packages/cashscript/src/Errors.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,30 @@ export class OutputTokenAmountTooSmallError extends Error {
2424
}
2525
}
2626

27+
export class OutputTokenCategoryInvalidError extends Error {
28+
constructor(category: string) {
29+
super(`Provided token category ${category} is not a hex string`);
30+
}
31+
}
32+
33+
export class OutputTokenCommitmentInvalidError extends Error {
34+
constructor(commitment: string) {
35+
super(`Provided token commitment ${commitment} is not a hex string`);
36+
}
37+
}
38+
2739
export class TokensToNonTokenAddressError extends Error {
2840
constructor(address: string) {
2941
super(`Tried to send tokens to an address without token support, ${address}.`);
3042
}
3143
}
3244

45+
export class OutputAddressNetworkMismatchError extends Error {
46+
constructor(address: string, expectedNetworkPrefix: string) {
47+
super(`Tried to add an output to an address on the wrong network, ${address}. Expected network prefix: ${expectedNetworkPrefix}.`);
48+
}
49+
}
50+
3351
export class NoDebugInformationInArtifactError extends Error {
3452
constructor() {
3553
super('No debug information found in artifact, please recompile with cashc version 0.10.0 or newer.');

packages/cashscript/src/TransactionBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class TransactionBuilder {
9696
}
9797

9898
addOutputs(outputs: Output[]): this {
99-
outputs.forEach(validateOutput);
99+
outputs.forEach((output) => validateOutput(output, this.provider.network));
100100
this.outputs = this.outputs.concat(outputs);
101101
return this;
102102
}

packages/cashscript/src/utils.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ import {
4242
OutputTokenAmountTooSmallError,
4343
TokensToNonTokenAddressError,
4444
UndefinedInputError,
45+
OutputAddressNetworkMismatchError,
46+
OutputTokenCategoryInvalidError,
47+
OutputTokenCommitmentInvalidError,
4548
} from './Errors.js';
4649

4750
// ////////// PARAMETER VALIDATION ////////////////////////////////////////////
@@ -51,25 +54,46 @@ export function validateInput(utxo: Utxo): void {
5154
}
5255
}
5356

54-
export function validateOutput(output: Output): void {
55-
if (typeof output.to !== 'string') return;
57+
export function validateOutput(output: Output, network: Network): void {
58+
if (isOpReturnOutput(output)) return;
5659

5760
const minimumAmount = calculateDust(output);
5861
if (output.amount < minimumAmount) {
5962
throw new OutputSatoshisTooSmallError(output.amount, BigInt(minimumAmount));
6063
}
6164

6265
if (output.token) {
63-
if (!isTokenAddress(output.to)) {
64-
throw new TokensToNonTokenAddressError(output.to);
65-
}
66-
6766
if (output.token.amount < 0n) {
6867
throw new OutputTokenAmountTooSmallError(output.token.amount);
6968
}
69+
70+
if (typeof output.token.category !== 'string' || !isHex(output.token.category)) {
71+
throw new OutputTokenCategoryInvalidError(output.token.category);
72+
}
73+
74+
if (output.token.nft && (typeof output.token.nft.commitment !== 'string' || !isHex(output.token.nft.commitment))) {
75+
throw new OutputTokenCommitmentInvalidError(output.token.nft.commitment);
76+
}
77+
}
78+
79+
// If the output is not an address (so it is P2S), then we don't need to do any address validation
80+
if (typeof output.to !== 'string') return;
81+
82+
if (output.token && !isTokenAddress(output.to)) {
83+
throw new TokensToNonTokenAddressError(output.to);
84+
}
85+
86+
const addressPrefix = getNetworkPrefixForAddress(output.to);
87+
const networkPrefix = getNetworkPrefix(network);
88+
if (addressPrefix !== networkPrefix) {
89+
throw new OutputAddressNetworkMismatchError(output.to, networkPrefix);
7090
}
7191
}
7292

93+
export function isOpReturnOutput(output: Output): boolean {
94+
return typeof output.to !== 'string' && output.to[0] === Op.OP_RETURN;
95+
}
96+
7397
export function calculateDust(output: Output): number {
7498
const outputSize = getOutputSize(output);
7599
// Formula used to calculate the minimum allowed output
@@ -87,16 +111,6 @@ export function encodeOutput(output: Output): Uint8Array {
87111
}
88112

89113
export function cashScriptOutputToLibauthOutput(output: Output): LibauthOutput {
90-
if (output.token) {
91-
if (typeof output.token.category !== 'string' || !isHex(output.token.category)) {
92-
throw new Error(`Provided token category ${output.token?.category} is not a hex string`);
93-
}
94-
95-
if (output.token.nft && (typeof output.token.nft.commitment !== 'string' || !isHex(output.token.nft.commitment))) {
96-
throw new Error(`Provided token commitment ${output.token.nft?.commitment} is not a hex string`);
97-
}
98-
}
99-
100114
return {
101115
lockingBytecode: typeof output.to === 'string' ? addressToLockScript(output.to) : output.to,
102116
valueSatoshis: output.amount,
@@ -150,6 +164,12 @@ function isTokenAddress(address: string): boolean {
150164
return supportsTokens;
151165
}
152166

167+
function getNetworkPrefixForAddress(address: string): string {
168+
const result = decodeCashAddress(address);
169+
if (typeof result === 'string') throw new Error(result);
170+
return result.prefix;
171+
}
172+
153173
// ////////// SIZE CALCULATIONS ///////////////////////////////////////////////
154174
export function getInputSize(inputScript: Uint8Array): number {
155175
const scriptSize = inputScript.byteLength;

0 commit comments

Comments
 (0)