Skip to content

Commit 55ee3c3

Browse files
nvrakesh06claude
andcommitted
feat(sdk-coin-polyx): add HexTransferBuilder for NEW Polymesh memo encoding (CECHO-1370)
Add HexTransferBuilder that produces the NEW Polymesh memo encoding: UTF-8 bytes right-padded with 0x00 to 32 bytes, passed as a 0x-prefixed hex string, instead of the OLD ASCII left-pad format used by TransferBuilder. Changes: - src/lib/hexTransferBuilder.ts: new class extending TransferBuilder, overrides memo() with NEW encoding; round-trip safe (already-encoded 0x strings stored as-is) - src/lib/utils.ts: add encodeMemoNew() and isNewMemoEncoding() helpers - src/lib/index.ts: export HexTransferBuilder - src/lib/transactionBuilderFactory.ts: add getHexTransferBuilder(); factory.from() auto-detects NEW memo encoding (any 0x00 byte) and returns HexTransferBuilder - src/polyx.ts: add hexTransfer() convenience method alongside stakingBatch()/bondExtra() - test/unit/transactionBuilder/hexTransferBuilder.ts: 31 unit tests covering memo encoding for all memo values, factory.from() routing, full round-trip (build → serialize → from() → build produces identical hex), and OLD vs NEW regression checks Verified on-chain: broadcast transferWithMemo with NEW memo "56594" on Polymesh testnet Tx: 0x41c453458385d1373343cb8b4fdc872acd73209881fd95f844ce060caab74d95 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1cf0be6 commit 55ee3c3

6 files changed

Lines changed: 508 additions & 1 deletion

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { TransferBuilder } from './transferBuilder';
3+
4+
/**
5+
* Builds a POLYX transferWithMemo transaction using the NEW Polymesh memo encoding:
6+
* UTF-8 bytes right-padded with 0x00 to 32 bytes, passed as a 0x-prefixed hex string.
7+
*
8+
* OLD encoding (TransferBuilder): plain string, left-padded with ASCII '0' to 32 chars.
9+
* NEW encoding (this class): 0x-prefixed hex, right-padded with 0x00 to 32 bytes.
10+
*
11+
* Same intended memo produces different on-chain bytes depending on which builder is used.
12+
* Use this builder when the recipient or downstream tooling expects Polymesh-native encoding.
13+
*/
14+
export class HexTransferBuilder extends TransferBuilder {
15+
constructor(_coinConfig: Readonly<CoinConfig>) {
16+
super(_coinConfig);
17+
}
18+
19+
/**
20+
* Encode memo using the NEW Polymesh format.
21+
*
22+
* If the value is already a 0x-prefixed 32-byte hex string (e.g. when decoding an
23+
* existing NEW-encoded transaction via fromImplementation), it is stored as-is.
24+
* Otherwise the text is converted to UTF-8 hex and right-padded with 0x00 to 32 bytes.
25+
*
26+
* @param memo The intended memo string (e.g. "56594", "testmemo")
27+
*/
28+
memo(memo: string): this {
29+
if (memo.startsWith('0x') && memo.length === 66) {
30+
// already a fully-encoded 32-byte hex memo — store as-is (round-trip from fromImplementation)
31+
this._memo = memo;
32+
} else {
33+
const textHex = Buffer.from(memo, 'utf8').toString('hex');
34+
this._memo = `0x${textHex.padEnd(64, '0')}`;
35+
}
36+
return this;
37+
}
38+
}

modules/sdk-coin-polyx/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
export { TransactionBuilderFactory } from './transactionBuilderFactory';
1212
export { PolyxBaseBuilder } from './baseBuilder';
1313
export { TransferBuilder } from './transferBuilder';
14+
export { HexTransferBuilder } from './hexTransferBuilder';
1415
export { RegisterDidWithCDDBuilder } from './registerDidWithCDDBuilder';
1516
export { PreApproveAssetBuilder } from './preApproveAssetBuilder';
1617
export { TokenTransferBuilder } from './tokenTransferBuilder';

modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BaseTransactionBuilderFactory, NotImplementedError } from '@bitgo/sdk-c
22
import { BaseCoin as CoinConfig } from '@bitgo/statics';
33
import { decode } from '@substrate/txwrapper-polkadot';
44
import { TransferBuilder } from './transferBuilder';
5+
import { HexTransferBuilder } from './hexTransferBuilder';
56
import { RegisterDidWithCDDBuilder } from './registerDidWithCDDBuilder';
67
import { BondExtraBuilder } from './bondExtraBuilder';
78
import { BatchStakingBuilder } from './batchStakingBuilder';
@@ -32,6 +33,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3233
return new TransferBuilder(this._coinConfig).material(this._material);
3334
}
3435

36+
getHexTransferBuilder(): HexTransferBuilder {
37+
return new HexTransferBuilder(this._coinConfig).material(this._material);
38+
}
39+
3540
getRegisterDidWithCDDBuilder(): RegisterDidWithCDDBuilder {
3641
return new RegisterDidWithCDDBuilder(this._coinConfig).material(this._material);
3742
}
@@ -96,6 +101,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
96101

97102
const methodName = decodedTxn.method?.name;
98103
if (methodName === Interface.MethodNames.TransferWithMemo) {
104+
const args = decodedTxn.method.args as Interface.TransferWithMemoArgs;
105+
if (utils.isNewMemoEncoding(args.memo)) {
106+
return this.getHexTransferBuilder();
107+
}
99108
return this.getTransferBuilder();
100109
} else if (methodName === MethodNames.RegisterDidWithCDD) {
101110
return this.getRegisterDidWithCDDBuilder();

modules/sdk-coin-polyx/src/lib/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ export class Utils extends SubstrateUtils {
2929
return POLYX_DID_REGEX.test(did);
3030
}
3131

32+
/**
33+
* Encode a memo string using the NEW Polymesh format:
34+
* UTF-8 bytes right-padded with 0x00 to 32 bytes, returned as a 0x-prefixed hex string.
35+
*
36+
* @param text The intended memo string
37+
* @returns 0x-prefixed 66-character hex string representing the 32-byte encoded memo
38+
*/
39+
encodeMemoNew(text: string): string {
40+
const textHex = Buffer.from(text, 'utf8').toString('hex');
41+
return `0x${textHex.padEnd(64, '0')}`;
42+
}
43+
44+
/**
45+
* Detect whether a decoded 32-byte memo (0x-prefixed hex from txwrapper decode()) uses NEW encoding.
46+
* NEW encoding right-pads with 0x00; any null byte means NEW. No null bytes → OLD (ASCII left-padded).
47+
*
48+
* @param memo 0x-prefixed 66-char hex string as returned by txwrapper decode()
49+
*/
50+
isNewMemoEncoding(memo: string): boolean {
51+
if (!memo.startsWith('0x') || memo.length !== 66) return false;
52+
const hex = memo.slice(2);
53+
for (let i = 0; i < hex.length - 1; i += 2) {
54+
if (hex[i] === '0' && hex[i + 1] === '0') return true;
55+
}
56+
return false;
57+
}
58+
3259
getMaterial(networkType: NetworkType): Interface.Material {
3360
return (networkType === NetworkType.MAINNET ? mainnetMaterial : testnetMaterial) as unknown as Interface.Material;
3461
}

modules/sdk-coin-polyx/src/polyx.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { BondExtraBuilder } from './lib/bondExtraBuilder';
2222
import { POLYX_ADDRESS_FORMAT } from './lib/constants';
2323
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
2424
import BigNumber from 'bignumber.js';
25-
import { TransactionBuilderFactory, TransferBuilder } from './lib';
25+
import { TransactionBuilderFactory, TransferBuilder, HexTransferBuilder } from './lib';
2626

2727
export class Polyx extends SubstrateCoin {
2828
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -75,6 +75,10 @@ export class Polyx extends SubstrateCoin {
7575
return this.getBuilder().getBondExtraBuilder();
7676
}
7777

78+
hexTransfer(): HexTransferBuilder {
79+
return this.getBuilder().getHexTransferBuilder();
80+
}
81+
7882
/**
7983
* Retrieves the address format for Polyx.
8084
*

0 commit comments

Comments
 (0)