@@ -24,6 +24,7 @@ import {
2424} from '@bitgo/sdk-core' ;
2525import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc' ;
2626import { BaseCoin as StaticsBaseCoin , coins } from '@bitgo/statics' ;
27+ import * as querystring from 'querystring' ;
2728import { TransactionBuilderFactory } from './lib' ;
2829import { KeyPair as CantonKeyPair } from './lib/keyPair' ;
2930import utils from './lib/utils' ;
@@ -36,6 +37,11 @@ export interface ExplainTransactionOptions {
3637 txHex : string ;
3738}
3839
40+ interface AddressDetails {
41+ address : string ;
42+ memoId ?: string ;
43+ }
44+
3945export class Canton extends BaseCoin {
4046 protected readonly _staticsCoin : Readonly < StaticsBaseCoin > ;
4147
@@ -119,10 +125,10 @@ export class Canton extends BaseCoin {
119125 case TransactionType . Send :
120126 if ( txParams . recipients !== undefined ) {
121127 const filteredRecipients = txParams . recipients ?. map ( ( recipient ) => {
122- const { address , amount, tokenName } = recipient ;
123- const [ addressPart , memoId ] = address . split ( '?memoId=' ) ;
128+ const { amount, tokenName } = recipient ;
129+ const { address , memoId } = this . getAddressDetails ( recipient . address ) ;
124130 return {
125- address : addressPart ,
131+ address,
126132 amount,
127133 ...( memoId && { memo : memoId } ) ,
128134 ...( tokenName && { tokenName } ) ,
@@ -153,7 +159,7 @@ export class Canton extends BaseCoin {
153159 // TODO: refactor this and use the `verifyEddsaMemoBasedWalletAddress` once published from sdk-core
154160 // https://bitgoinc.atlassian.net/browse/COIN-6347
155161 const { keychains, address : newAddress , index } = params ;
156- const [ addressPart , memoId ] = newAddress . split ( '?memoId=' ) ;
162+ const { address : addressPart , memoId } = this . getAddressDetails ( newAddress ) ;
157163 if ( ! this . isValidAddress ( addressPart ) ) {
158164 throw new InvalidAddressError ( `invalid address: ${ newAddress } ` ) ;
159165 }
@@ -214,6 +220,56 @@ export class Canton extends BaseCoin {
214220 return utils . isValidAddress ( address ) ;
215221 }
216222
223+ /**
224+ * Process address into address and optional memo id
225+ *
226+ * @param address the address
227+ * @returns object containing base address and optional memo id
228+ */
229+ getAddressDetails ( address : string ) : AddressDetails {
230+ const queryIndex = address . indexOf ( '?' ) ;
231+ const destinationAddress = queryIndex >= 0 ? address . slice ( 0 , queryIndex ) : address ;
232+ const query = queryIndex >= 0 ? address . slice ( queryIndex + 1 ) : undefined ;
233+
234+ // Address without memoId query parameter.
235+ if ( query === undefined ) {
236+ return {
237+ address,
238+ memoId : undefined ,
239+ } ;
240+ }
241+
242+ if ( ! query || destinationAddress . length === 0 ) {
243+ throw new InvalidAddressError ( `invalid address: ${ address } ` ) ;
244+ }
245+
246+ const queryDetails = querystring . parse ( query ) ;
247+ if ( ! queryDetails . memoId ) {
248+ throw new InvalidAddressError ( `invalid address: ${ address } ` ) ;
249+ }
250+
251+ if ( Array . isArray ( queryDetails . memoId ) ) {
252+ throw new InvalidAddressError (
253+ `memoId may only be given at most once, but found ${ queryDetails . memoId . length } instances in address ${ address } `
254+ ) ;
255+ }
256+
257+ const queryKeys = Object . keys ( queryDetails ) ;
258+ if ( queryKeys . length !== 1 ) {
259+ throw new InvalidAddressError ( `invalid address: ${ address } ` ) ;
260+ }
261+
262+ const [ memoId ] = [ queryDetails . memoId ] . filter ( ( value ) : value is string => typeof value === 'string' ) ;
263+ if ( ! memoId || memoId . trim ( ) . length === 0 ) {
264+ throw new InvalidAddressError ( `invalid address: ${ address } ` ) ;
265+ }
266+
267+ return {
268+ address : destinationAddress ,
269+ memoId,
270+ } ;
271+ }
272+
217273 /** @inheritDoc */
218274 getTokenEnablementConfig ( ) : TokenEnablementConfig {
219275 return {
0 commit comments