diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index 0d62ba6566..ca04c7a288 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -29,6 +29,7 @@ import { AdaTokenConfig, AlgoTokenConfig, TrxTokenConfig, + XrpMptTokenConfig, XrpTokenConfig, SuiTokenConfig, AptTokenConfig, @@ -222,6 +223,7 @@ import { XdcToken, Xlm, Xrp, + XrpMptToken, XrpToken, Xtz, Zec, @@ -543,6 +545,10 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin ({ name, coinConstructor }) => coinFactory.register(name, coinConstructor) ); + XrpMptToken.createTokenConstructors([...tokens.bitcoin.xrp.mptTokens, ...tokens.testnet.xrp.mptTokens]).forEach( + ({ name, coinConstructor }) => coinFactory.register(name, coinConstructor) + ); + AptToken.createTokenConstructors([...tokens.bitcoin.apt.tokens, ...tokens.testnet.apt.tokens]).forEach( ({ name, coinConstructor }) => coinFactory.register(name, coinConstructor) ); @@ -1065,6 +1071,9 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | return PolyxToken.createTokenConstructor(tokenConfig as PolyxTokenConfig); case 'xrp': case 'txrp': + if ('canTransfer' in tokenConfig) { + return XrpMptToken.createTokenConstructor(tokenConfig as XrpMptTokenConfig); + } return XrpToken.createTokenConstructor(tokenConfig as XrpTokenConfig); case 'apt': case 'tapt': diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 13b82c2d46..cbbe8fdb50 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -72,7 +72,7 @@ import { Vet, Tvet, VetToken } from '@bitgo/sdk-coin-vet'; import { Wemix, Twemix } from '@bitgo/sdk-coin-wemix'; import { World, Tworld, WorldToken } from '@bitgo/sdk-coin-world'; import { Xdc, Txdc, XdcToken } from '@bitgo/sdk-coin-xdc'; -import { Txrp, Xrp, XrpToken } from '@bitgo/sdk-coin-xrp'; +import { Txrp, Xrp, XrpMptToken, XrpToken } from '@bitgo/sdk-coin-xrp'; import { Txtz, Xtz } from '@bitgo/sdk-coin-xtz'; import { Tzec, Zec } from '@bitgo/sdk-coin-zec'; import { Tzeta, Zeta } from '@bitgo/sdk-coin-zeta'; @@ -150,7 +150,7 @@ export { Trx, Ttrx }; export { Vet, Tvet, VetToken }; export { Xdc, Txdc, XdcToken }; export { StellarToken, Txlm, Xlm }; -export { Txrp, Xrp, XrpToken }; +export { Txrp, Xrp, XrpMptToken, XrpToken }; export { Txtz, Xtz }; export { Tzec, Zec }; export { Tzeta, Zeta }; diff --git a/modules/bitgo/test/browser/browser.spec.ts b/modules/bitgo/test/browser/browser.spec.ts index c0cfb421d7..28afc09fe6 100644 --- a/modules/bitgo/test/browser/browser.spec.ts +++ b/modules/bitgo/test/browser/browser.spec.ts @@ -35,6 +35,7 @@ describe('Coins', () => { TaoToken: 1, PolyxToken: 1, BeraToken: 1, + XrpMptToken: 1, XrpToken: 1, Rune: 1, Trune: 1, diff --git a/modules/sdk-coin-xrp/src/index.ts b/modules/sdk-coin-xrp/src/index.ts index b452b44656..5188def986 100644 --- a/modules/sdk-coin-xrp/src/index.ts +++ b/modules/sdk-coin-xrp/src/index.ts @@ -3,3 +3,4 @@ export * from './register'; export * from './txrp'; export * from './xrp'; export * from './xrpToken'; +export * from './xrpMptToken'; diff --git a/modules/sdk-coin-xrp/src/register.ts b/modules/sdk-coin-xrp/src/register.ts index d105900142..0b1ae31640 100644 --- a/modules/sdk-coin-xrp/src/register.ts +++ b/modules/sdk-coin-xrp/src/register.ts @@ -2,6 +2,7 @@ import { BitGoBase } from '@bitgo/sdk-core'; import { Txrp } from './txrp'; import { Xrp } from './xrp'; import { XrpToken } from './xrpToken'; +import { XrpMptToken } from './xrpMptToken'; export const register = (sdk: BitGoBase): void => { sdk.register('xrp', Xrp.createInstance); @@ -9,4 +10,7 @@ export const register = (sdk: BitGoBase): void => { XrpToken.createTokenConstructors().forEach(({ name, coinConstructor }) => { sdk.register(name, coinConstructor); }); + XrpMptToken.createTokenConstructors().forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + }); }; diff --git a/modules/sdk-coin-xrp/src/xrpMptToken.ts b/modules/sdk-coin-xrp/src/xrpMptToken.ts new file mode 100644 index 0000000000..5cb388e79e --- /dev/null +++ b/modules/sdk-coin-xrp/src/xrpMptToken.ts @@ -0,0 +1,66 @@ +import { coins, XrpMptTokenConfig, tokens } from '@bitgo/statics'; +import { Xrp } from './xrp'; +import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core'; + +export class XrpMptToken extends Xrp { + public readonly tokenConfig: XrpMptTokenConfig; + + constructor(bitgo: BitGoBase, tokenConfig: XrpMptTokenConfig) { + const staticsCoin = tokenConfig.network === 'Mainnet' ? coins.get('xrp') : coins.get('txrp'); + super(bitgo, staticsCoin); + this.tokenConfig = tokenConfig; + } + + static createTokenConstructor(config: XrpMptTokenConfig): CoinConstructor { + return (bitgo: BitGoBase) => new XrpMptToken(bitgo, config); + } + + static createTokenConstructors( + tokenConfigs: XrpMptTokenConfig[] = [...tokens.bitcoin.xrp.mptTokens, ...tokens.testnet.xrp.mptTokens] + ): NamedCoinConstructor[] { + return tokenConfigs.map((config) => ({ + name: config.type, + coinConstructor: XrpMptToken.createTokenConstructor(config), + })); + } + + get name(): string { + return this.tokenConfig.name; + } + + get coin(): string { + return this.tokenConfig.coin; + } + + get network(): string { + return this.tokenConfig.network; + } + + get contractAddress(): string { + return this.tokenConfig.contractAddress; + } + + get canTransfer(): boolean { + return this.tokenConfig.canTransfer; + } + + get decimalPlaces(): number { + return this.tokenConfig.decimalPlaces; + } + + getChain(): string { + return this.tokenConfig.type; + } + + getBaseChain(): string { + return this.coin; + } + + getFullName(): string { + return 'XRP MPT Token'; + } + + getBaseFactor(): number { + return Math.pow(10, this.tokenConfig.decimalPlaces); + } +} diff --git a/modules/sdk-coin-xrp/test/unit/xrpToken.ts b/modules/sdk-coin-xrp/test/unit/xrpToken.ts index 2acac32a25..6d60eae330 100644 --- a/modules/sdk-coin-xrp/test/unit/xrpToken.ts +++ b/modules/sdk-coin-xrp/test/unit/xrpToken.ts @@ -2,7 +2,7 @@ import 'should'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { BitGoAPI } from '@bitgo/sdk-api'; -import { XrpToken } from '../../src'; +import { XrpMptToken, XrpToken } from '../../src'; describe('Xrp Tokens', function () { let bitgo: TestBitGoAPI; @@ -35,3 +35,52 @@ describe('Xrp Tokens', function () { xrpTokenCoin.currencyCode.should.equal('524C555344000000000000000000000000000000'); }); }); + +describe('Xrp MPT Tokens', function () { + let bitgo: TestBitGoAPI; + let mptCoin: XrpMptToken; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' }); + XrpMptToken.createTokenConstructors().forEach(({ name, coinConstructor }) => { + bitgo.safeRegister(name, coinConstructor); + }); + bitgo.initializeTestVars(); + mptCoin = bitgo.coin('txrp:sec0') as XrpMptToken; + }); + + it('should register all testnet MPT tokens', function () { + const names = XrpMptToken.createTokenConstructors().map(({ name }) => name); + names.should.containEql('txrp:sec0'); + names.should.containEql('txrp:sec2'); + names.should.containEql('txrp:wrapt'); + names.should.containEql('txrp:ntsec'); + names.should.containEql('txrp:feesec'); + }); + + it('should return constants for txrp:sec0', function () { + mptCoin.getChain().should.equal('txrp:sec0'); + mptCoin.getBaseChain().should.equal('txrp'); + mptCoin.getFullName().should.equal('XRP MPT Token'); + mptCoin.getBaseFactor().should.equal(1); // assetScale 0 → 10^0 + mptCoin.coin.should.equal('txrp'); + mptCoin.network.should.equal('Testnet'); + mptCoin.decimalPlaces.should.equal(0); + mptCoin.contractAddress.should.equal('01135794225BAA3A7F9DA001AF93FB258C517F50E20DE771'); + mptCoin.canTransfer.should.equal(true); + }); + + it('should reflect canTransfer=false for non-transferable tokens', function () { + const ntsec = bitgo.coin('txrp:ntsec') as XrpMptToken; + ntsec.canTransfer.should.equal(false); + ntsec.contractAddress.should.equal('01135791225BAA3A7F9DA001AF93FB258C517F50E20DE771'); + }); + + it('should return correct baseFactor for tokens with non-zero assetScale', function () { + const sec2 = bitgo.coin('txrp:sec2') as XrpMptToken; // assetScale 2 + sec2.getBaseFactor().should.equal(100); // 10^2 + + const wrapt = bitgo.coin('txrp:wrapt') as XrpMptToken; // assetScale 8 + wrapt.getBaseFactor().should.equal(100000000); // 10^8 + }); +}); diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index c89354ca52..5dfcd0ba7b 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -48,6 +48,7 @@ import { ttronToken, tvetNFTCollection, tworldErc20, + txrpMptToken, txrpToken, tzkethErc20, vetNFTCollection, @@ -7553,6 +7554,51 @@ export const allCoinsAndTokens = [ 'straitsx.com', UnderlyingAsset['txrp:xsgd'] ), + txrpMptToken( + 'b8016d32-12d1-4bbd-b0f7-5e543557c0f7', + 'txrp:sec0', + 'Test Security Token 0dp', + '01135794225BAA3A7F9DA001AF93FB258C517F50E20DE771', + true, + 0, + UnderlyingAsset['txrp:sec0'] + ), + txrpMptToken( + '5df3833f-74c4-4607-bccf-1bc061b1b092', + 'txrp:sec2', + 'Test Security Token 2dp', + '0113578F225BAA3A7F9DA001AF93FB258C517F50E20DE771', + true, + 2, + UnderlyingAsset['txrp:sec2'] + ), + txrpMptToken( + '44d89e3d-4938-41bf-bc4d-a8fcd3ea34b2', + 'txrp:wrapt', + 'Test Wrapped Token', + '01135790225BAA3A7F9DA001AF93FB258C517F50E20DE771', + true, + 8, + UnderlyingAsset['txrp:wrapt'] + ), + txrpMptToken( + '18c9a83b-b147-41cb-9586-e8e966948422', + 'txrp:ntsec', + 'Test Non-Transferable Security', + '01135791225BAA3A7F9DA001AF93FB258C517F50E20DE771', + false, + 2, + UnderlyingAsset['txrp:ntsec'] + ), + txrpMptToken( + '2bad14c2-e6b8-41ea-a204-17ceeb501daf', + 'txrp:feesec', + 'Test Fee Security Token', + '01135792225BAA3A7F9DA001AF93FB258C517F50E20DE771', + true, + 6, + UnderlyingAsset['txrp:feesec'] + ), suiToken( 'f26941b7-1110-4aa7-a2bc-29807297a51c', 'sui:deep', diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 86cc9ebfb8..7fb9a59632 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -3706,6 +3706,12 @@ export enum UnderlyingAsset { 'xrp:fiuaxrp' = 'xrp:fiuaxrp', // XRP testnet tokens 'txrp:xsgd' = 'txrp:xsgd', + // XRP MPT testnet tokens + 'txrp:sec0' = 'txrp:sec0', + 'txrp:sec2' = 'txrp:sec2', + 'txrp:wrapt' = 'txrp:wrapt', + 'txrp:ntsec' = 'txrp:ntsec', + 'txrp:feesec' = 'txrp:feesec', // Sui tokens 'sui:deep' = 'sui:deep',