Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions modules/sdk-coin-polyx/src/lib/hexTokenTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import utils from './utils';
*
* OLD encoding (TokenTransferBuilder): plain string, left-padded with ASCII '0' to 32 chars.
* NEW encoding (this class): 0x-prefixed hex, right-padded with 0x00 to 32 bytes.
*
* [CLEANUP-V8-OLD] v7 args shape (bare PortfolioId). Kept for Flipt rollback alongside
* V8HexTokenTransferBuilder (AssetHolder-wrapped legs/holderSet).
*/
export class HexTokenTransferBuilder extends TokenTransferBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-coin-polyx/src/lib/hexTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import utils from './utils';
*
* Same intended memo produces different on-chain bytes depending on which builder is used.
* Use this builder when the recipient or downstream tooling expects Polymesh-native encoding.
*
* [CLEANUP-V8-OLD] v7 metadata. Kept for Flipt rollback alongside V8HexTransferBuilder.
*/
export class HexTransferBuilder extends TransferBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down
89 changes: 89 additions & 0 deletions modules/sdk-coin-polyx/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,19 @@ export const MethodNames = {

/**
* Registers a Decentralized Identifier (DID) along with Customer Due Diligence (CDD) information.
* [CLEANUP-V8-OLD] v7 CDD-provider path; still present and callable on v8 at the same call
* index (0x0714). Kept for Flipt rollback alongside the v8 DID Registrar path (RegisterDid).
*
* @see https://developers.polymesh.network/sdk-docs/enums/Generated/Types/IdentityTx/#cddregisterdidwithcdd
*/
RegisterDidWithCDD: 'cddRegisterDidWithCdd' as const,

/**
* v8 DID Registrar path — registers a DID without a CDD claim. BitGo acts as the DID
* Registrar and signs with the platform key, same operational model as RegisterDidWithCDD.
*/
RegisterDid: 'registerDid' as const,

/**
* Pre-approves an asset.
*/
Expand All @@ -60,6 +68,12 @@ export const MethodNames = {
AddAndAffirmWithMediators: 'addAndAffirmWithMediators' as const,

RejectInstruction: 'rejectInstruction' as const,

/** v8 — balances.transfer was renamed transferAllowDeath (same call slot 0x0500). */
TransferAllowDeath: 'transferAllowDeath' as const,

/** v8 — balances.transferKeepAlive moved to a new call index (0x0503). */
TransferKeepAlive: 'transferKeepAlive' as const,
} as const;

// Create a type that represents the keys of this object
Expand All @@ -68,16 +82,29 @@ export type MethodNamesType = keyof typeof MethodNames;
// Create a type that represents the values of this object
export type MethodNamesValues = (typeof MethodNames)[MethodNamesType];

// [CLEANUP-V8-OLD] v7 CDD path args (identity.cddRegisterDidWithCdd). Kept for Flipt rollback.
export interface RegisterDidWithCDDArgs extends Args {
targetAccount: string;
secondaryKeys: [];
expiry: null;
}

/**
* v8 identity.registerDid args. NOTE: this call takes only `targetAccount` — it does NOT carry
* `secondaryKeys`/`expiry` the way the v7 CDD path does. Verified against the real testnetV8Material
* metadata (registerDid's call fields are `[target_account]` only); the migration plan's dry-run
* note claiming an identical {targetAccount, secondaryKeys, expiry} shape does not hold.
*/
export interface RegisterDidArgs extends Args {
targetAccount: string;
}

export interface PreApproveAssetArgs extends Args {
assetId: string;
}

// [CLEANUP-V8-OLD] v7 settlement.addAndAffirmWithMediators args — portfolios/legs use bare
// PortfolioId. v8 replaces this with V8AddAndAffirmWithMediatorsArgs (AssetHolder-wrapped, below).
export interface AddAndAffirmWithMediatorsArgs extends Args {
venueId: null;
settlementType: SettlementType.SettleOnAffirmation;
Expand All @@ -96,6 +123,66 @@ export interface AddAndAffirmWithMediatorsArgs extends Args {
mediators: [];
}

/**
* v8 AssetHolder enum — wraps the DID+kind that identifies a fungible-asset holder.
* Confirmed on Polymesh Testnet v8 (block 24801173): settlement legs / holderSet now use
* this wrapper instead of the bare v7 PortfolioId shape. Only the `Portfolio` variant is
* used by BitGo's portfolio-based flows; `Account` covers account-based ownership (out of
* scope for this migration).
*/
export type AssetHolder = { Portfolio: { did: string; kind: PortfolioKind.Default } } | { Account: string };

/**
* v8 settlement.addAndAffirmWithMediators args. Call index is unchanged (0x2514) but
* `portfolios` is renamed `holderSet` and every leg sender/receiver is wrapped in AssetHolder.
*/
export interface V8AddAndAffirmWithMediatorsArgs extends Args {
venueId: null;
settlementType: SettlementType.SettleOnAffirmation;
tradeDate: null;
valueDate: null;
legs: Array<{
fungible: {
sender: AssetHolder;
receiver: AssetHolder;
assetId: string;
amount: string;
};
}>;
holderSet: AssetHolder[];
instructionMemo: string;
mediators: [];
}

/**
* Decoded shape of AssetHolder. SCALE decoding (txwrapper-core's `decode()`, used by
* `fromImplementation`/`validateDecodedTransaction`) lowercases the enum variant name and
* expands `kind` to `{ default: null }` — verified by round-tripping a sample value through
* the real testnetV8Material metadata registry. This differs from the construction shape
* (`AssetHolder` above, capitalized `Portfolio`/`Account`) used when building a transaction.
*/
export type DecodedAssetHolder = { portfolio: { did: string; kind: { default: null } } } | { account: string };

/** Decoded shape of V8AddAndAffirmWithMediatorsArgs — see DecodedAssetHolder for why this differs
* from the construction-side type. */
export interface DecodedV8AddAndAffirmWithMediatorsArgs extends Args {
venueId: null;
settlementType: { settleOnAffirmation: null };
tradeDate: null;
valueDate: null;
legs: Array<{
fungible: {
sender: DecodedAssetHolder;
receiver: DecodedAssetHolder;
assetId: string;
amount: string;
};
}>;
holderSet: DecodedAssetHolder[];
instructionMemo: string;
mediators: [];
}

export interface RejectInstructionBuilderArgs extends Args {
id: string;
portfolio: { did: string; kind: PortfolioKind.Default };
Expand All @@ -116,8 +203,10 @@ export interface TxMethod extends Omit<Interface.TxMethod, 'args' | 'name'> {
| Interface.WithdrawUnbondedArgs
| Interface.BatchArgs
| RegisterDidWithCDDArgs
| RegisterDidArgs
| PreApproveAssetArgs
| AddAndAffirmWithMediatorsArgs
| V8AddAndAffirmWithMediatorsArgs
| RejectInstructionBuilderArgs;
name: MethodNamesValues;
}
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-polyx/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { WithdrawUnbondedBuilder } from './withdrawUnbondedBuilder';
export { V8TransferBuilder } from './v8TransferBuilder';
export { V8HexTransferBuilder } from './v8HexTransferBuilder';
export { V8RegisterDidWithCDDBuilder } from './v8RegisterDidWithCDDBuilder';
export { V8RegisterDidBuilder } from './v8RegisterDidBuilder';
export { V8TokenTransferBuilder } from './v8TokenTransferBuilder';
export { V8HexTokenTransferBuilder } from './v8HexTokenTransferBuilder';
export { V8PreApproveAssetBuilder } from './v8PreApproveAssetBuilder';
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-polyx/src/lib/preApproveAssetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { TxMethod, PreApproveAssetArgs, MethodNames } from './iface';
import { PreApproveAssetTransactionSchema } from './txnSchema';
import { Transaction } from './transaction';

// [CLEANUP-V8-OLD] v7 metadata. Kept for Flipt rollback alongside V8PreApproveAssetBuilder.
export class PreApproveAssetBuilder extends PolyxBaseBuilder<TxMethod, Transaction> {
protected _assetId: string;
protected _method: TxMethod;
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-polyx/src/lib/registerDidWithCDDBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { RegisterDidWithCDDArgs, TxMethod, MethodNames } from './iface';
import { RegisterDidWithCDDTransactionSchema } from './txnSchema';
import { Transaction } from './transaction';

// [CLEANUP-V8-OLD] v7 CDD-provider path (identity.cddRegisterDidWithCdd, 0x0714). Still present
// and callable on v8 at the same call index — kept for Flipt rollback alongside the v8 DID
// Registrar path (V8RegisterDidBuilder, identity.registerDid @ 0x0718).
export class RegisterDidWithCDDBuilder extends PolyxBaseBuilder<TxMethod, Transaction> {
protected _to: string;
protected _method: TxMethod;
Expand Down
22 changes: 16 additions & 6 deletions modules/sdk-coin-polyx/src/lib/tokenTransferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Interface } from '@bitgo/abstract-substrate';
import { AddAndAffirmWithMediatorsTransactionSchema } from './txnSchema';
import { DecodedSignedTx, DecodedSigningPayload, defineMethod, UnsignedTransaction } from '@substrate/txwrapper-core';

// [CLEANUP-V8-OLD] v7 settlement.addAndAffirmWithMediators — bare PortfolioId legs/portfolios.
// Kept for Flipt rollback alongside V8TokenTransferBuilder (AssetHolder-wrapped legs/holderSet).
export class TokenTransferBuilder extends PolyxBaseBuilder<TxMethod, Transaction> {
protected _assetId: string;
protected _amount: string;
Expand Down Expand Up @@ -121,18 +123,26 @@ export class TokenTransferBuilder extends PolyxBaseBuilder<TxMethod, Transaction
protected fromImplementation(rawTransaction: string): Transaction {
const tx = super.fromImplementation(rawTransaction);
if (this._method?.name === MethodNames.AddAndAffirmWithMediators) {
const txMethod = this._method.args as AddAndAffirmWithMediatorsArgs;
this.assetId(txMethod.legs[0].fungible.assetId);
this.amount(txMethod.legs[0].fungible.amount);
this.memo(txMethod.instructionMemo);
this.fromDID(txMethod.legs[0].fungible.sender.did);
this.toDID(txMethod.legs[0].fungible.receiver.did);
this.populateFromMethodArgs(this._method.args as AddAndAffirmWithMediatorsArgs);
} else {
throw new Error(`Invalid Transaction Type: ${this._method?.name}. Expected AddAndAffirmWithMediators`);
}
return tx;
}

/**
* Populates builder state from decoded method args. Extracted as its own method so
* V8TokenTransferBuilder can override just the AssetHolder-shaped field access while
* reusing the rest of fromImplementation.
*/
protected populateFromMethodArgs(txMethod: AddAndAffirmWithMediatorsArgs): void {
this.assetId(txMethod.legs[0].fungible.assetId);
this.amount(txMethod.legs[0].fungible.amount);
this.memo(txMethod.instructionMemo);
this.fromDID(txMethod.legs[0].fungible.sender.did);
this.toDID(txMethod.legs[0].fungible.receiver.did);
}

/** @inheritdoc */
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction?: string): void {
if (decodedTxn.method?.name === MethodNames.AddAndAffirmWithMediators) {
Expand Down
29 changes: 23 additions & 6 deletions modules/sdk-coin-polyx/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@ import {
PreApproveAssetArgs,
TxData,
AddAndAffirmWithMediatorsArgs,
DecodedV8AddAndAffirmWithMediatorsArgs,
RejectInstructionBuilderArgs,
} from './iface';
import polyxUtils from './utils';

/**
* Reads the DID out of a decoded settlement leg holder. This Transaction class is shared by
* TokenTransferBuilder (v7, bare `{ did, kind }`) and V8TokenTransferBuilder (v8, AssetHolder-
* wrapped `{ portfolio: { did, kind } }`) — both are decoded here, so both shapes must be
* supported. `Account`-variant AssetHolders (v8 account-based ownership) are out of scope.
*/
function extractPortfolioDID(holder: { did: string } | { portfolio: { did: string } } | { account: string }): string {
if ('did' in holder) {
return holder.did;
}
if ('portfolio' in holder) {
return holder.portfolio.did;
}
throw new InvalidTransactionError('Unsupported settlement leg holder shape (expected did or portfolio.did)');
}

export class Transaction extends SubstrateTransaction {
/**
* Override the getAddressFormat method to return different values based on network type
Expand Down Expand Up @@ -88,9 +105,9 @@ export class Transaction extends SubstrateTransaction {
result.sender = decodedTx.address;
result.amount = '0'; // Pre-approval does not transfer any value
} else if (this.type === TransactionType.SendToken) {
const sendTokenArgs = txMethod as AddAndAffirmWithMediatorsArgs;
result.fromDID = sendTokenArgs.legs[0].fungible.sender.did;
result.toDID = sendTokenArgs.legs[0].fungible.receiver.did;
const sendTokenArgs = txMethod as AddAndAffirmWithMediatorsArgs | DecodedV8AddAndAffirmWithMediatorsArgs;
result.fromDID = extractPortfolioDID(sendTokenArgs.legs[0].fungible.sender);
result.toDID = extractPortfolioDID(sendTokenArgs.legs[0].fungible.receiver);
result.amount = sendTokenArgs.legs[0].fungible.amount.toString();
result.assetId = sendTokenArgs.legs[0].fungible.assetId;
result.memo = sendTokenArgs.instructionMemo;
Expand Down Expand Up @@ -173,9 +190,9 @@ export class Transaction extends SubstrateTransaction {
}

private decodeInputsAndOutputsForSendToken(decodedTx: DecodedTx) {
const txMethod = decodedTx.method.args as AddAndAffirmWithMediatorsArgs;
const fromDID = txMethod.legs[0].fungible.sender.did;
const toDID = txMethod.legs[0].fungible.receiver.did;
const txMethod = decodedTx.method.args as AddAndAffirmWithMediatorsArgs | DecodedV8AddAndAffirmWithMediatorsArgs;
const fromDID = extractPortfolioDID(txMethod.legs[0].fungible.sender);
const toDID = extractPortfolioDID(txMethod.legs[0].fungible.receiver);
const amount = txMethod.legs[0].fungible.amount.toString();
const assetId = txMethod.legs[0].fungible.assetId;
const tokenName = this.getTokenNameByAssetId(assetId);
Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { NominateBuilder } from './nominateBuilder';
import { V8TransferBuilder } from './v8TransferBuilder';
import { V8HexTransferBuilder } from './v8HexTransferBuilder';
import { V8RegisterDidWithCDDBuilder } from './v8RegisterDidWithCDDBuilder';
import { V8RegisterDidBuilder } from './v8RegisterDidBuilder';
import { V8TokenTransferBuilder } from './v8TokenTransferBuilder';
import { V8HexTokenTransferBuilder } from './v8HexTokenTransferBuilder';
import { V8PreApproveAssetBuilder } from './v8PreApproveAssetBuilder';
Expand Down Expand Up @@ -100,6 +101,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return new V8RegisterDidWithCDDBuilder(this._coinConfig);
}

getV8RegisterDidBuilder(): V8RegisterDidBuilder {
return new V8RegisterDidBuilder(this._coinConfig);
}

getV8TokenTransferBuilder(): V8TokenTransferBuilder {
return new V8TokenTransferBuilder(this._coinConfig);
}
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-coin-polyx/src/lib/transferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { BaseAddress, InvalidTransactionError, TransactionType } from '@bitgo/sd

import utils from './utils';

// [CLEANUP-V8-OLD] v7 balances.transferWithMemo builder (call index 0x0501). Kept for Flipt
// rollback alongside V8TransferBuilder (v8 metadata; call index 0x0528).
export class TransferBuilder extends PolyxBaseBuilder<TxMethod, Transaction> {
protected _amount: string;
protected _to: string;
Expand Down
51 changes: 51 additions & 0 deletions modules/sdk-coin-polyx/src/lib/txnSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export const RegisterDidWithCDDTransactionSchema = joi.object({
expiry: joi.valid(null).required(),
});

// v8 identity.registerDid — unlike the v7 CDD path, this call only carries targetAccount
// (verified against the real testnetV8Material metadata; no secondaryKeys/expiry fields).
export const RegisterDidTransactionSchema = joi.object({
targetAccount: addressSchema.required(),
});

export const PreApproveAssetTransactionSchema = joi.object({
assetId: joi.string().required(),
});
Expand Down Expand Up @@ -93,6 +99,51 @@ export const AddAndAffirmWithMediatorsTransactionSchema = joi.object({
mediators: joi.array().length(0).required(),
});

// v8 settlement.addAndAffirmWithMediators — legs/holderSet wrap the DID+kind in an AssetHolder
// enum. Decoded shape lowercases the variant name (`portfolio`, not `Portfolio`) — verified by
// round-tripping a sample value through the real v8 testnet metadata registry.
const assetHolderSchema = joi.object({
portfolio: joi
.object({
did: addressSchema.required(),
kind: joi
.object({
default: joi.valid(null),
})
.required(),
})
.required(),
});

export const V8AddAndAffirmWithMediatorsTransactionSchema = joi.object({
venueId: joi.valid(null).required(),
settlementType: joi
.object({
settleOnAffirmation: joi.valid(null),
})
.required(),
tradeDate: joi.valid(null).required(),
valueDate: joi.valid(null).required(),
legs: joi
.array()
.items(
joi.object({
fungible: joi
.object({
sender: assetHolderSchema.required(),
receiver: assetHolderSchema.required(),
assetId: joi.string().required(),
amount: joi.number().required(),
})
.required(),
})
)
.required(),
holderSet: joi.array().items(assetHolderSchema).required(),
instructionMemo: joi.string().required(),
mediators: joi.array().length(0).required(),
});

export const RejectInstructionTransactionSchema = joi.object({
id: joi.string().required(),
portfolio: joi
Expand Down
Loading
Loading