diff --git a/modules/sdk-coin-stx/src/lib/utils.ts b/modules/sdk-coin-stx/src/lib/utils.ts index c82fc52498..9e56737e3f 100644 --- a/modules/sdk-coin-stx/src/lib/utils.ts +++ b/modules/sdk-coin-stx/src/lib/utils.ts @@ -21,8 +21,10 @@ import { deserializeTransaction, PubKeyEncoding, publicKeyFromSignature, + serializeCV, signWithKey, StacksTransaction, + standardPrincipalCV, TransactionVersion, validateStacksAddress, } from '@stacks/transactions'; @@ -505,6 +507,33 @@ export function getAddressVersion(address: string): AddressVersion { return createAddress(baseAddress).version; } +/** + * Encode a Stacks *standard* principal as the Clarity SIP-005 hex wire format + * — a 22-byte blob: + * + * `0x05` || version (1 byte) || hash160 (20 bytes) + * + * Contract principals (`
.`) and addresses with a + * `?memoId=…` suffix are rejected. The Clarity type byte (`0x05`) is set + * by `serializeCV` from `@stacks/transactions`, so this helper does not + * hand-roll any byte concatenation. + * + * @param {string} principal a Stacks standard principal (e.g. `SP…` / `ST…`) + * @returns {string} hex-encoded 22-byte Clarity standard principal + */ +export function getEncodedPrincipal(principal: string): string { + if (principal.includes('?')) { + throw new UtilsError(`principal must not include a query string: ${principal}`); + } + if (principal.includes('.')) { + throw new UtilsError(`contract principals are not supported, expected a standard principal: ${principal}`); + } + if (!isValidAddress(principal)) { + throw new UtilsError(`invalid Stacks address in principal: ${principal}`); + } + return serializeCV(standardPrincipalCV(principal)).toString('hex'); +} + /** * Returns a STX pub key from an xpub * diff --git a/modules/sdk-coin-stx/test/unit/util.ts b/modules/sdk-coin-stx/test/unit/util.ts index ef4fe40954..f26fdff1ea 100644 --- a/modules/sdk-coin-stx/test/unit/util.ts +++ b/modules/sdk-coin-stx/test/unit/util.ts @@ -323,6 +323,32 @@ describe('Stx util library', function () { }); }); + describe('getEncodedPrincipal', function () { + it('should encode a testnet standard principal as the 22-byte Clarity blob', function () { + Utils.getEncodedPrincipal('ST390D0WBF60T25P36KMCP6WN1BXQ7TTMMQKNV15C').should.equal( + '051ad206838b7981a116c334e8cb1b950afb73eb54a5' + ); + }); + + it('should reject contract principals', function () { + should.throws( + () => Utils.getEncodedPrincipal('ST390D0WBF60T25P36KMCP6WN1BXQ7TTMMQKNV15C.my-contract'), + /contract principals are not supported/ + ); + }); + + it('should reject addresses with a memoId suffix', function () { + should.throws( + () => Utils.getEncodedPrincipal('ST390D0WBF60T25P36KMCP6WN1BXQ7TTMMQKNV15C?memoId=0'), + /must not include a query string/ + ); + }); + + it('should reject invalid addresses', function () { + should.throws(() => Utils.getEncodedPrincipal('not-a-stacks-address'), /invalid Stacks address/); + }); + }); + describe('xpubToSTXPubkey', function () { it('should succeed to convert for valid xpubs', function () { Utils.xpubToSTXPubkey(