Skip to content

Commit 4e36e26

Browse files
committed
fix: Add missing post-condition conversion functions: Pc.fromHex(), wireToPostCondition
1 parent 4dcad2c commit 4e36e26

File tree

4 files changed

+289
-19
lines changed

4 files changed

+289
-19
lines changed

packages/transactions/src/pc.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import {
55
FungiblePostCondition,
66
NonFungibleComparator,
77
NonFungiblePostCondition,
8+
PostCondition,
89
StxPostCondition,
910
} from './postcondition-types';
1011
import { AddressString, AssetString, ContractIdString } from './types';
11-
import {} from './postcondition';
1212
import { parseContractId, validateStacksAddress } from './utils';
13+
import { deserializePostConditionWire } from './wire';
14+
import { wireToPostCondition } from './postcondition';
1315

1416
/// `Pc.` Post Condition Builder
1517
//
@@ -266,6 +268,29 @@ function parseNft(nftAssetName: AssetString) {
266268
return { contractAddress: address, contractName: name, tokenName };
267269
}
268270

271+
/**
272+
* Deserializes a serialized post condition hex string into a post condition object
273+
* @param hex - Post condition hex string
274+
* @returns Deserialized post condition
275+
* @example
276+
* ```ts
277+
* import { Pc } from '@stacks/transactions';
278+
*
279+
* const hex = '00021600000000000000000000000000000000000000000200000000000003e8'
280+
* const postCondition = Pc.fromHex(hex);
281+
* // {
282+
* // type: 'stx-postcondition',
283+
* // address: 'SP000000000000000000002Q6VF78',
284+
* // condition: 'gt',
285+
* // amount: '1000'
286+
* // }
287+
* ```
288+
*/
289+
export function fromHex(hex: string): PostCondition {
290+
const wire = deserializePostConditionWire(hex);
291+
return wireToPostCondition(wire);
292+
}
293+
269294
/**
270295
* Helper method for `PartialPcNftWithCode.nft` to parse the arguments.
271296
* @internal

packages/transactions/src/postcondition.ts

+103-16
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,36 @@ import {
55
PostConditionPrincipalId,
66
PostConditionType,
77
} from './constants';
8-
import { PostCondition, PostConditionModeName } from './postcondition-types';
98
import {
9+
FungibleComparator,
10+
NonFungibleComparator,
11+
PostCondition,
12+
PostConditionModeName,
13+
} from './postcondition-types';
14+
import { AssetString } from './types';
15+
import {
16+
AssetWire,
17+
PostConditionPrincipalWire,
1018
PostConditionWire,
1119
StacksWireType,
20+
addressToString,
1221
parseAssetString,
1322
parsePrincipalString,
1423
serializePostConditionWire,
1524
} from './wire';
1625

17-
const FUNGIBLE_COMPARATOR_MAPPING = {
18-
eq: FungibleConditionCode.Equal,
19-
gt: FungibleConditionCode.Greater,
20-
lt: FungibleConditionCode.Less,
21-
gte: FungibleConditionCode.GreaterEqual,
22-
lte: FungibleConditionCode.LessEqual,
23-
};
26+
/** @internal */
27+
enum PostConditionCodeWireType {
28+
eq = FungibleConditionCode.Equal,
29+
gt = FungibleConditionCode.Greater,
30+
lt = FungibleConditionCode.Less,
31+
gte = FungibleConditionCode.GreaterEqual,
32+
lte = FungibleConditionCode.LessEqual,
2433

25-
const NON_FUNGIBLE_COMPARATOR_MAPPING = {
26-
sent: NonFungibleConditionCode.Sends,
27-
'not-sent': NonFungibleConditionCode.DoesNotSend,
28-
};
34+
sent = NonFungibleConditionCode.Sends,
35+
'not-sent' = NonFungibleConditionCode.DoesNotSend,
36+
}
2937

30-
/** @ignore */
3138
export function postConditionToWire(postcondition: PostCondition): PostConditionWire {
3239
switch (postcondition.type) {
3340
case 'stx-postcondition':
@@ -38,7 +45,7 @@ export function postConditionToWire(postcondition: PostCondition): PostCondition
3845
postcondition.address === 'origin'
3946
? { type: StacksWireType.Principal, prefix: PostConditionPrincipalId.Origin }
4047
: parsePrincipalString(postcondition.address),
41-
conditionCode: FUNGIBLE_COMPARATOR_MAPPING[postcondition.condition],
48+
conditionCode: conditionTypeToByte(postcondition.condition) as FungibleConditionCode,
4249
amount: BigInt(postcondition.amount),
4350
};
4451
case 'ft-postcondition':
@@ -49,7 +56,7 @@ export function postConditionToWire(postcondition: PostCondition): PostCondition
4956
postcondition.address === 'origin'
5057
? { type: StacksWireType.Principal, prefix: PostConditionPrincipalId.Origin }
5158
: parsePrincipalString(postcondition.address),
52-
conditionCode: FUNGIBLE_COMPARATOR_MAPPING[postcondition.condition],
59+
conditionCode: conditionTypeToByte(postcondition.condition) as FungibleConditionCode,
5360
amount: BigInt(postcondition.amount),
5461
asset: parseAssetString(postcondition.asset),
5562
};
@@ -61,7 +68,7 @@ export function postConditionToWire(postcondition: PostCondition): PostCondition
6168
postcondition.address === 'origin'
6269
? { type: StacksWireType.Principal, prefix: PostConditionPrincipalId.Origin }
6370
: parsePrincipalString(postcondition.address),
64-
conditionCode: NON_FUNGIBLE_COMPARATOR_MAPPING[postcondition.condition],
71+
conditionCode: conditionTypeToByte(postcondition.condition),
6572
asset: parseAssetString(postcondition.asset),
6673
assetName: postcondition.assetId,
6774
};
@@ -70,6 +77,63 @@ export function postConditionToWire(postcondition: PostCondition): PostCondition
7077
}
7178
}
7279

80+
export function wireToPostCondition(wire: PostConditionWire): PostCondition {
81+
switch (wire.conditionType) {
82+
case PostConditionType.STX:
83+
return {
84+
type: 'stx-postcondition',
85+
address: principalWireToString(wire.principal),
86+
condition: conditionByteToType(wire.conditionCode),
87+
amount: wire.amount.toString(),
88+
};
89+
case PostConditionType.Fungible:
90+
return {
91+
type: 'ft-postcondition',
92+
address: principalWireToString(wire.principal),
93+
condition: conditionByteToType(wire.conditionCode),
94+
amount: wire.amount.toString(),
95+
asset: assetWireToString(wire.asset),
96+
};
97+
case PostConditionType.NonFungible:
98+
return {
99+
type: 'nft-postcondition',
100+
address: principalWireToString(wire.principal),
101+
condition: conditionByteToType(wire.conditionCode),
102+
asset: assetWireToString(wire.asset),
103+
assetId: wire.assetName,
104+
};
105+
default: {
106+
const _exhaustiveCheck: never = wire;
107+
throw new Error(`Invalid post condition type: ${_exhaustiveCheck}`);
108+
}
109+
}
110+
}
111+
112+
/** @internal */
113+
export function conditionTypeToByte<T extends FungibleComparator | NonFungibleComparator>(
114+
condition: T
115+
): T extends FungibleComparator ? FungibleConditionCode : NonFungibleConditionCode {
116+
return (
117+
PostConditionCodeWireType as unknown as Record<
118+
T,
119+
T extends FungibleComparator ? FungibleConditionCode : NonFungibleConditionCode
120+
>
121+
)[condition];
122+
}
123+
124+
/** @internal */
125+
export function conditionByteToType<T extends FungibleConditionCode | NonFungibleConditionCode>(
126+
wireType: T
127+
): T extends FungibleConditionCode ? FungibleComparator : NonFungibleComparator {
128+
return (
129+
PostConditionCodeWireType as unknown as Record<
130+
// numerical enums are bidirectional in TypeScript
131+
T,
132+
T extends FungibleConditionCode ? FungibleComparator : NonFungibleComparator
133+
>
134+
)[wireType];
135+
}
136+
73137
/**
74138
* Convert a post condition to a hex string
75139
* @param postcondition - The post condition object to convert
@@ -102,3 +166,26 @@ export function postConditionModeFrom(
102166
if (mode === 'deny') return PostConditionMode.Deny;
103167
throw new Error(`Invalid post condition mode: ${mode}`);
104168
}
169+
170+
/** @internal */
171+
function assetWireToString(asset: AssetWire): AssetString {
172+
const address = addressToString(asset.address);
173+
const contractId = `${address}.${asset.contractName.content}` as const;
174+
return `${contractId}::${asset.assetName.content}`;
175+
}
176+
177+
/** @internal */
178+
function principalWireToString(principal: PostConditionPrincipalWire): string {
179+
switch (principal.prefix) {
180+
case PostConditionPrincipalId.Origin:
181+
return 'origin';
182+
case PostConditionPrincipalId.Standard:
183+
return addressToString(principal.address);
184+
case PostConditionPrincipalId.Contract:
185+
const address = addressToString(principal.address);
186+
return `${address}.${principal.contractName.content}`;
187+
default:
188+
const _exhaustiveCheck: never = principal;
189+
throw new Error(`Invalid principal type: ${_exhaustiveCheck}`);
190+
}
191+
}

packages/transactions/tests/pc.test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -417,4 +417,20 @@ describe('pc -- post condition builder', () => {
417417
});
418418
});
419419
});
420+
421+
describe('fromHex function', () => {
422+
test('deserializes hex string to post condition object', () => {
423+
const hex = '00021600000000000000000000000000000000000000000200000000000003e8';
424+
const postCondition = Pc.fromHex(hex);
425+
426+
const expectedPostCondition: StxPostCondition = {
427+
type: 'stx-postcondition',
428+
address: 'SP000000000000000000002Q6VF78',
429+
condition: 'gt',
430+
amount: '1000',
431+
};
432+
433+
expect(postCondition).toEqual(expectedPostCondition);
434+
});
435+
});
420436
});

packages/transactions/tests/postcondition.test.ts

+144-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
ContractPrincipalWire,
55
FungiblePostConditionWire,
66
NonFungiblePostConditionWire,
7+
Pc,
78
STXPostConditionWire,
8-
StacksWireType,
99
addressToString,
1010
deserializeTransaction,
1111
} from '../src';
@@ -16,7 +16,14 @@ import {
1616
PostConditionPrincipalId,
1717
PostConditionType,
1818
} from '../src/constants';
19-
import { postConditionToHex, postConditionToWire } from '../src/postcondition';
19+
import {
20+
conditionByteToType,
21+
conditionTypeToByte,
22+
postConditionToHex,
23+
postConditionToWire,
24+
wireToPostCondition,
25+
} from '../src/postcondition';
26+
import { StacksWireType, parseAssetString, parsePrincipalString } from '../src/wire';
2027
import { serializeDeserialize } from './macros';
2128

2229
test('STX post condition serialization and deserialization', () => {
@@ -224,3 +231,138 @@ describe('origin postcondition', () => {
224231
}).not.toThrow();
225232
});
226233
});
234+
235+
describe('wireToPostCondition', () => {
236+
const TEST_CASES = [
237+
{
238+
name: 'STX post condition',
239+
postConditionWire: {
240+
type: StacksWireType.PostCondition,
241+
conditionType: PostConditionType.STX,
242+
principal: parsePrincipalString('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B'),
243+
conditionCode: FungibleConditionCode.GreaterEqual,
244+
amount: 1000000n,
245+
},
246+
expected: Pc.principal('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B')
247+
.willSendGte(1000000n)
248+
.ustx(),
249+
},
250+
{
251+
name: 'STX post condition with origin',
252+
postConditionWire: {
253+
type: StacksWireType.PostCondition,
254+
conditionType: PostConditionType.STX,
255+
principal: { type: StacksWireType.Principal, prefix: PostConditionPrincipalId.Origin },
256+
conditionCode: FungibleConditionCode.Equal,
257+
amount: 2000000n,
258+
},
259+
expected: Pc.origin().willSendEq(2000000n).ustx(),
260+
},
261+
{
262+
name: 'Fungible post condition',
263+
postConditionWire: {
264+
type: StacksWireType.PostCondition,
265+
conditionType: PostConditionType.Fungible,
266+
principal: parsePrincipalString('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B'),
267+
conditionCode: FungibleConditionCode.GreaterEqual,
268+
amount: 1000000n,
269+
asset: parseAssetString(
270+
'SP2ZP4GJDZJ1FDHTQ963F0292PE9J9752TZJ68F21.contract_name::asset_name'
271+
),
272+
},
273+
expected: Pc.principal('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B')
274+
.willSendGte(1000000n)
275+
.ft('SP2ZP4GJDZJ1FDHTQ963F0292PE9J9752TZJ68F21.contract_name', 'asset_name'),
276+
},
277+
{
278+
name: 'Non-fungible post condition',
279+
postConditionWire: {
280+
type: StacksWireType.PostCondition,
281+
conditionType: PostConditionType.NonFungible,
282+
principal: parsePrincipalString('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B.contract-name'),
283+
conditionCode: NonFungibleConditionCode.DoesNotSend,
284+
asset: parseAssetString(
285+
'SP2ZP4GJDZJ1FDHTQ963F0292PE9J9752TZJ68F21.contract_name::asset_name'
286+
),
287+
assetName: bufferCVFromString('nft_asset_name'),
288+
},
289+
expected: Pc.principal('SP2JXKMSH007NPYAQHKJPQMAQYAD90NQGTVJVQ02B.contract-name')
290+
.willNotSendAsset()
291+
.nft(
292+
'SP2ZP4GJDZJ1FDHTQ963F0292PE9J9752TZJ68F21.contract_name',
293+
'asset_name',
294+
bufferCVFromString('nft_asset_name')
295+
),
296+
},
297+
] as const;
298+
299+
test.each(TEST_CASES)('$name', ({ postConditionWire, expected }) => {
300+
const postCondition = wireToPostCondition(postConditionWire);
301+
expect(postCondition).toEqual(expected);
302+
303+
const roundTrip = postConditionToWire(postCondition);
304+
expect(roundTrip).toEqual(postConditionWire);
305+
});
306+
});
307+
308+
describe('conditionTypeToByte', () => {
309+
const fungibleTestCases = [
310+
{ name: 'eq', expectedCode: FungibleConditionCode.Equal },
311+
{ name: 'gt', expectedCode: FungibleConditionCode.Greater },
312+
{ name: 'lt', expectedCode: FungibleConditionCode.Less },
313+
{ name: 'gte', expectedCode: FungibleConditionCode.GreaterEqual },
314+
{ name: 'lte', expectedCode: FungibleConditionCode.LessEqual },
315+
] as const;
316+
317+
const nonFungibleTestCases = [
318+
{ name: 'sent', expectedCode: NonFungibleConditionCode.Sends },
319+
{ name: 'not-sent', expectedCode: NonFungibleConditionCode.DoesNotSend },
320+
] as const;
321+
322+
test.each(fungibleTestCases)(
323+
'converts fungible condition $name to byte code',
324+
({ name, expectedCode }) => {
325+
const result = conditionTypeToByte(name);
326+
expect(result).toBe(expectedCode);
327+
}
328+
);
329+
330+
test.each(nonFungibleTestCases)(
331+
'converts non-fungible condition $name to byte code',
332+
({ name, expectedCode }) => {
333+
const result = conditionTypeToByte(name);
334+
expect(result).toBe(expectedCode);
335+
}
336+
);
337+
});
338+
339+
describe('conditionBytesToType', () => {
340+
const fungibleTestCases = [
341+
{ code: FungibleConditionCode.Equal, expectedName: 'eq' },
342+
{ code: FungibleConditionCode.Greater, expectedName: 'gt' },
343+
{ code: FungibleConditionCode.Less, expectedName: 'lt' },
344+
{ code: FungibleConditionCode.GreaterEqual, expectedName: 'gte' },
345+
{ code: FungibleConditionCode.LessEqual, expectedName: 'lte' },
346+
] as const;
347+
348+
const nonFungibleTestCases = [
349+
{ code: NonFungibleConditionCode.Sends, expectedName: 'sent' },
350+
{ code: NonFungibleConditionCode.DoesNotSend, expectedName: 'not-sent' },
351+
] as const;
352+
353+
test.each(fungibleTestCases)(
354+
'converts fungible condition code $code to name',
355+
({ code, expectedName }) => {
356+
const result = conditionByteToType(code);
357+
expect(result).toBe(expectedName);
358+
}
359+
);
360+
361+
test.each(nonFungibleTestCases)(
362+
'converts non-fungible condition code $code to name',
363+
({ code, expectedName }) => {
364+
const result = conditionByteToType(code);
365+
expect(result).toBe(expectedName);
366+
}
367+
);
368+
});

0 commit comments

Comments
 (0)