Skip to content

Commit 13e3231

Browse files
committed
feat(sdk-coin-xrp): add MPT transaction builders
Ticket: CGD-1470 TICKET: CGD-1471
1 parent bb74580 commit 13e3231

12 files changed

Lines changed: 715 additions & 29 deletions

File tree

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

Lines changed: 20 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,9 @@ export interface TxData {
138153
// signer list set fields
139154
signerQuorum?: number;
140155
signerEntries?: SignerEntry[];
156+
// mpt fields
157+
mptIssuanceId?: string;
158+
mptAmount?: MPTAmount;
141159
}
142160

143161
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 { MPTokenAuthorizeBuilder } from './mpTokenAuthorizeBuilder';
9+
export { MptTransferBuilder } from './mptTransferBuilder';
810
export { TokenTransferBuilder } from './tokenTransferBuilder';
911
export { Transaction } from './transaction';
1012
export { TransactionBuilder } from './transactionBuilder';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
8+
export class MPTokenAuthorizeBuilder extends TransactionBuilder {
9+
private _mptIssuanceId?: string;
10+
private _mptHolder?: string;
11+
12+
constructor(_coinConfig: Readonly<CoinConfig>) {
13+
super(_coinConfig);
14+
}
15+
16+
protected get transactionType(): TransactionType {
17+
return TransactionType.MPTokenAuthorize;
18+
}
19+
20+
protected get xrpTransactionType(): XrpTransactionType.MPTokenAuthorize {
21+
return XrpTransactionType.MPTokenAuthorize;
22+
}
23+
24+
/**
25+
* Set the MPTokenIssuanceID to authorize.
26+
* @param {string} id - 48-character hex MPTokenIssuanceID
27+
*/
28+
mptIssuanceId(id: string): this {
29+
if (!/^[0-9a-fA-F]{48}$/.test(id)) {
30+
throw new BuildTransactionError('MPTokenIssuanceID must be a 48-character hex string (192 bits)');
31+
}
32+
this._mptIssuanceId = id;
33+
return this;
34+
}
35+
36+
/**
37+
* Set the Holder field for issuer-side authorization (Phase 2 only).
38+
* Omit for standard holder self-authorization.
39+
* @param {string} address - the holder account address
40+
*/
41+
mptHolder(address: string): this {
42+
this._mptHolder = address;
43+
return this;
44+
}
45+
46+
initBuilder(tx: Transaction): void {
47+
super.initBuilder(tx);
48+
const { mptIssuanceId } = tx.toJson();
49+
if (mptIssuanceId) {
50+
this._mptIssuanceId = mptIssuanceId;
51+
}
52+
}
53+
54+
/** @inheritdoc */
55+
protected async buildImplementation(): Promise<Transaction> {
56+
if (!this._sender) {
57+
throw new BuildTransactionError('Sender must be set before building the transaction');
58+
}
59+
if (!this._mptIssuanceId) {
60+
throw new BuildTransactionError('MPTokenIssuanceID must be set before building the transaction');
61+
}
62+
63+
const authorizeFields: MPTokenAuthorize = {
64+
TransactionType: this.xrpTransactionType,
65+
Account: this._sender,
66+
MPTokenIssuanceID: this._mptIssuanceId,
67+
};
68+
69+
// Omit Holder for self-authorization — setting it causes XRPL rejection on holder self-auth.
70+
if (this._mptHolder) {
71+
authorizeFields.Holder = this._mptHolder;
72+
}
73+
74+
this._specificFields = authorizeFields;
75+
76+
return await super.buildImplementation();
77+
}
78+
}
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 MptTransferBuilder 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)