Skip to content

Commit 291a6fb

Browse files
Merge pull request #8891 from BitGo/CGD-1471-mpt-sdk-builders
feat(sdk-coin-xrp): add MPT transaction builders
2 parents 7c2b633 + c04303a commit 291a6fb

12 files changed

Lines changed: 744 additions & 29 deletions

File tree

modules/sdk-coin-xrp/src/lib/iface.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,32 @@ import {
55
VerifyAddressOptions as BaseVerifyAddressOptions,
66
TransactionPrebuild,
77
} from '@bitgo/sdk-core';
8-
import { AccountDelete, AccountSet, Amount, Payment, Signer, SignerEntry, SignerListSet, TrustSet } from 'xrpl';
8+
import {
9+
AccountDelete,
10+
AccountSet,
11+
Amount,
12+
MPTAmount,
13+
MPTokenAuthorize,
14+
Payment,
15+
Signer,
16+
SignerEntry,
17+
SignerListSet,
18+
TrustSet,
19+
} from 'xrpl';
920

1021
export enum XrpTransactionType {
1122
AccountDelete = 'AccountDelete',
1223
AccountSet = 'AccountSet',
1324
Payment = 'Payment',
1425
SignerListSet = 'SignerListSet',
1526
TrustSet = 'TrustSet',
27+
MPTokenAuthorize = 'MPTokenAuthorize',
1628
}
1729

18-
export type XrpTransaction = AccountDelete | Payment | AccountSet | SignerListSet | TrustSet;
30+
// Re-export so consumers can import alongside other XRP types from this module.
31+
export type { MPTAmount, MPTokenAuthorize };
32+
33+
export type XrpTransaction = AccountDelete | Payment | AccountSet | SignerListSet | TrustSet | MPTokenAuthorize;
1934

2035
export interface Address {
2136
address: string;
@@ -138,6 +153,10 @@ export interface TxData {
138153
// signer list set fields
139154
signerQuorum?: number;
140155
signerEntries?: SignerEntry[];
156+
// mpt fields
157+
mptIssuanceId?: string;
158+
mptHolder?: string; // issuer-side auth only (Phase 2) — absent for holder self-auth
159+
mptAmount?: MPTAmount;
141160
}
142161

143162
export interface SignerDetails {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export { AccountSetBuilder } from './accountSetBuilder';
55
export * from './constants';
66
export * from './iface';
77
export { KeyPair } from './keyPair';
8+
export { MPTAuthorizeBuilder } from './mpTokenAuthorizeBuilder';
9+
export { MPTokenTransferBuilder } from './mptTransferBuilder';
810
export { TokenTransferBuilder } from './tokenTransferBuilder';
911
export { Transaction } from './transaction';
1012
export { TransactionBuilder } from './transactionBuilder';
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { MPTokenAuthorize } from 'xrpl';
4+
import { XrpTransactionType } from './iface';
5+
import { Transaction } from './transaction';
6+
import { TransactionBuilder } from './transactionBuilder';
7+
import utils from './utils';
8+
9+
export class MPTAuthorizeBuilder extends TransactionBuilder {
10+
private _mptIssuanceId?: string;
11+
private _mptHolder?: string;
12+
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
}
16+
17+
protected get transactionType(): TransactionType {
18+
return TransactionType.MPTokenAuthorize;
19+
}
20+
21+
protected get xrpTransactionType(): XrpTransactionType.MPTokenAuthorize {
22+
return XrpTransactionType.MPTokenAuthorize;
23+
}
24+
25+
/**
26+
* Set the MPTokenIssuanceID to authorize.
27+
* @param {string} id - 48-character hex MPTokenIssuanceID
28+
*/
29+
mptIssuanceId(id: string): this {
30+
if (!/^[0-9a-fA-F]{48}$/.test(id)) {
31+
throw new BuildTransactionError('MPTokenIssuanceID must be a 48-character hex string (192 bits)');
32+
}
33+
this._mptIssuanceId = id;
34+
return this;
35+
}
36+
37+
/**
38+
* Set the Holder field for issuer-side authorization (Phase 2 only).
39+
* Omit for standard holder self-authorization.
40+
* @param {string} address - the holder account address (must be a valid XRP address)
41+
*/
42+
mptHolder(address: string): this {
43+
if (!utils.isValidAddress(address)) {
44+
throw new BuildTransactionError('Invalid holder address: ' + address);
45+
}
46+
this._mptHolder = address;
47+
return this;
48+
}
49+
50+
initBuilder(tx: Transaction): void {
51+
super.initBuilder(tx);
52+
const { mptIssuanceId, mptHolder } = tx.toJson();
53+
if (mptIssuanceId) {
54+
this._mptIssuanceId = mptIssuanceId;
55+
}
56+
if (mptHolder) {
57+
this._mptHolder = mptHolder;
58+
}
59+
}
60+
61+
/** @inheritdoc */
62+
protected async buildImplementation(): Promise<Transaction> {
63+
if (!this._sender) {
64+
throw new BuildTransactionError('Sender must be set before building the transaction');
65+
}
66+
if (!this._mptIssuanceId) {
67+
throw new BuildTransactionError('MPTokenIssuanceID must be set before building the transaction');
68+
}
69+
70+
const authorizeFields: MPTokenAuthorize = {
71+
TransactionType: this.xrpTransactionType,
72+
Account: this._sender,
73+
MPTokenIssuanceID: this._mptIssuanceId,
74+
};
75+
76+
// Omit Holder for self-authorization — setting it causes XRPL rejection on holder self-auth.
77+
if (this._mptHolder) {
78+
authorizeFields.Holder = this._mptHolder;
79+
}
80+
81+
this._specificFields = authorizeFields;
82+
83+
return await super.buildImplementation();
84+
}
85+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { MPTAmount, Payment } from 'xrpl';
4+
import { XrpTransactionType } from './iface';
5+
import { Transaction } from './transaction';
6+
import { TransactionBuilder } from './transactionBuilder';
7+
import utils from './utils';
8+
9+
export class MPTokenTransferBuilder extends TransactionBuilder {
10+
private _mptIssuanceId?: string;
11+
private _value?: string;
12+
private _destination?: string;
13+
private _destinationTag?: number;
14+
15+
constructor(_coinConfig: Readonly<CoinConfig>) {
16+
super(_coinConfig);
17+
}
18+
19+
protected get transactionType(): TransactionType {
20+
return TransactionType.SendMPT;
21+
}
22+
23+
protected get xrpTransactionType(): XrpTransactionType.Payment {
24+
// MPT transfers use a standard Payment transaction with an MPT Amount object
25+
return XrpTransactionType.Payment;
26+
}
27+
28+
/**
29+
* Set the recipient address (with optional destination tag).
30+
* @param {string} address - the recipient XRP address, optionally with destination tag
31+
*/
32+
to(address: string): this {
33+
const { address: xrpAddress, destinationTag } = utils.getAddressDetails(address);
34+
this._destination = xrpAddress;
35+
this._destinationTag = destinationTag;
36+
return this;
37+
}
38+
39+
/**
40+
* Set the MPT issuance ID and raw integer amount to transfer.
41+
* The value is a raw integer string — AssetScale is display-only and never applied here.
42+
* @param {string} issuanceId - 48-character hex MPTokenIssuanceID
43+
* @param {string} value - raw integer string (e.g. "1000" = 1000 base units)
44+
*/
45+
mptAmount(issuanceId: string, value: string): this {
46+
if (!/^[0-9a-fA-F]{48}$/.test(issuanceId)) {
47+
throw new BuildTransactionError('MPTokenIssuanceID must be a 48-character hex string');
48+
}
49+
if (!/^\d+$/.test(value)) {
50+
throw new BuildTransactionError('MPT value must be a non-negative integer string');
51+
}
52+
this._mptIssuanceId = issuanceId;
53+
this._value = value;
54+
return this;
55+
}
56+
57+
initBuilder(tx: Transaction): void {
58+
super.initBuilder(tx);
59+
const { destination, destinationTag, mptAmount } = tx.toJson();
60+
if (destination) {
61+
const normalizedAddress = utils.normalizeAddress({ address: destination, destinationTag });
62+
this.to(normalizedAddress);
63+
}
64+
if (mptAmount) {
65+
this.mptAmount(mptAmount.mpt_issuance_id, mptAmount.value);
66+
}
67+
}
68+
69+
/** @inheritdoc */
70+
protected async buildImplementation(): Promise<Transaction> {
71+
if (!this._sender) {
72+
throw new BuildTransactionError('Sender must be set before building the transaction');
73+
}
74+
if (!this._destination || !this._mptIssuanceId || !this._value) {
75+
throw new BuildTransactionError(
76+
'Missing mandatory MPT payment parameters: destination, mptIssuanceId, and value are all required'
77+
);
78+
}
79+
80+
const mptAmountObj: MPTAmount = {
81+
mpt_issuance_id: this._mptIssuanceId,
82+
value: this._value,
83+
};
84+
85+
const paymentFields: Payment = {
86+
TransactionType: this.xrpTransactionType,
87+
Account: this._sender,
88+
Destination: this._destination,
89+
Amount: mptAmountObj,
90+
};
91+
92+
if (typeof this._destinationTag === 'number') {
93+
paymentFields.DestinationTag = this._destinationTag;
94+
}
95+
96+
this._specificFields = paymentFields;
97+
98+
return await super.buildImplementation();
99+
}
100+
}

0 commit comments

Comments
 (0)