Skip to content

Commit ed6e69b

Browse files
Merge pull request #8316 from BitGo/CHALO-77
fix: add getAddressDetails method for canton
2 parents 76a84bf + 46b529a commit ed6e69b

2 files changed

Lines changed: 91 additions & 5 deletions

File tree

modules/sdk-coin-canton/src/canton.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '@bitgo/sdk-core';
2525
import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc';
2626
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
27+
import * as querystring from 'querystring';
2728
import { TransactionBuilderFactory } from './lib';
2829
import { KeyPair as CantonKeyPair } from './lib/keyPair';
2930
import 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+
3945
export 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 {
Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,45 @@
1-
import 'should';
1+
import should from 'should';
22
import { BitGoAPI } from '@bitgo/sdk-api';
3+
import { InvalidAddressError } from '@bitgo/sdk-core';
34
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
45
import { Canton, Tcanton } from '../../src';
6+
import { CANTON_ADDRESSES } from '../resources';
57

68
describe('Canton:', function () {
79
let bitgo: TestBitGoAPI;
10+
let basecoin: Canton;
811

912
before(function () {
1013
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
1114
bitgo.safeRegister('canton', Canton.createInstance);
1215
bitgo.safeRegister('tcanton', Tcanton.createInstance);
1316
bitgo.initializeTestVars();
17+
basecoin = bitgo.coin('canton') as Canton;
18+
});
19+
20+
describe('getAddressDetails', function () {
21+
it('should get address details without memoId', function () {
22+
const addressDetails = basecoin.getAddressDetails(CANTON_ADDRESSES.VALID_ADDRESS);
23+
addressDetails.address.should.equal(CANTON_ADDRESSES.VALID_ADDRESS);
24+
should.not.exist(addressDetails.memoId);
25+
});
26+
27+
it('should get address details with memoId', function () {
28+
const addressWithMemoId = CANTON_ADDRESSES.VALID_MEMO_ID;
29+
const addressDetails = basecoin.getAddressDetails(addressWithMemoId);
30+
addressDetails.address.should.equal(CANTON_ADDRESSES.VALID_ADDRESS);
31+
should.exist(addressDetails.memoId);
32+
addressDetails.memoId!.should.equal('1');
33+
});
34+
35+
it('should throw on multiple memoId query params', function () {
36+
(() => basecoin.getAddressDetails(`${CANTON_ADDRESSES.VALID_ADDRESS}?memoId=1&memoId=2`)).should.throw(
37+
InvalidAddressError
38+
);
39+
});
40+
41+
it('should throw on unknown query params', function () {
42+
(() => basecoin.getAddressDetails(`${CANTON_ADDRESSES.VALID_ADDRESS}?foo=bar`)).should.throw(InvalidAddressError);
43+
});
1444
});
1545
});

0 commit comments

Comments
 (0)