Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions modules/sdk-coin-canton/src/canton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@bitgo/sdk-core';
import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc';
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import * as querystring from 'querystring';
import { TransactionBuilderFactory } from './lib';
import { KeyPair as CantonKeyPair } from './lib/keyPair';
import utils from './lib/utils';
Expand All @@ -36,6 +37,11 @@ export interface ExplainTransactionOptions {
txHex: string;
}

interface AddressDetails {
address: string;
memoId?: string;
}

export class Canton extends BaseCoin {
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;

Expand Down Expand Up @@ -119,10 +125,10 @@ export class Canton extends BaseCoin {
case TransactionType.Send:
if (txParams.recipients !== undefined) {
const filteredRecipients = txParams.recipients?.map((recipient) => {
const { address, amount, tokenName } = recipient;
const [addressPart, memoId] = address.split('?memoId=');
const { amount, tokenName } = recipient;
const { address, memoId } = this.getAddressDetails(recipient.address);
return {
address: addressPart,
address,
amount,
...(memoId && { memo: memoId }),
...(tokenName && { tokenName }),
Expand Down Expand Up @@ -153,7 +159,7 @@ export class Canton extends BaseCoin {
// TODO: refactor this and use the `verifyEddsaMemoBasedWalletAddress` once published from sdk-core
// https://bitgoinc.atlassian.net/browse/COIN-6347
const { keychains, address: newAddress, index } = params;
const [addressPart, memoId] = newAddress.split('?memoId=');
const { address: addressPart, memoId } = this.getAddressDetails(newAddress);
if (!this.isValidAddress(addressPart)) {
throw new InvalidAddressError(`invalid address: ${newAddress}`);
}
Expand Down Expand Up @@ -214,6 +220,56 @@ export class Canton extends BaseCoin {
return utils.isValidAddress(address);
}

/**
* Process address into address and optional memo id
*
* @param address the address
* @returns object containing base address and optional memo id
*/
getAddressDetails(address: string): AddressDetails {
const queryIndex = address.indexOf('?');
const destinationAddress = queryIndex >= 0 ? address.slice(0, queryIndex) : address;
const query = queryIndex >= 0 ? address.slice(queryIndex + 1) : undefined;

// Address without memoId query parameter.
if (query === undefined) {
return {
address,
memoId: undefined,
};
}

if (!query || destinationAddress.length === 0) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

const queryDetails = querystring.parse(query);
if (!queryDetails.memoId) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

if (Array.isArray(queryDetails.memoId)) {
throw new InvalidAddressError(
`memoId may only be given at most once, but found ${queryDetails.memoId.length} instances in address ${address}`
);
}

const queryKeys = Object.keys(queryDetails);
if (queryKeys.length !== 1) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

const [memoId] = [queryDetails.memoId].filter((value): value is string => typeof value === 'string');
if (!memoId || memoId.trim().length === 0) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

return {
address: destinationAddress,
memoId,
};
}

/** @inheritDoc */
getTokenEnablementConfig(): TokenEnablementConfig {
return {
Expand Down
32 changes: 31 additions & 1 deletion modules/sdk-coin-canton/test/unit/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
import 'should';
import should from 'should';
import { BitGoAPI } from '@bitgo/sdk-api';
import { InvalidAddressError } from '@bitgo/sdk-core';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { Canton, Tcanton } from '../../src';
import { CANTON_ADDRESSES } from '../resources';

describe('Canton:', function () {
let bitgo: TestBitGoAPI;
let basecoin: Canton;

before(function () {
bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
bitgo.safeRegister('canton', Canton.createInstance);
bitgo.safeRegister('tcanton', Tcanton.createInstance);
bitgo.initializeTestVars();
basecoin = bitgo.coin('canton') as Canton;
});

describe('getAddressDetails', function () {
it('should get address details without memoId', function () {
const addressDetails = basecoin.getAddressDetails(CANTON_ADDRESSES.VALID_ADDRESS);
addressDetails.address.should.equal(CANTON_ADDRESSES.VALID_ADDRESS);
should.not.exist(addressDetails.memoId);
});

it('should get address details with memoId', function () {
const addressWithMemoId = CANTON_ADDRESSES.VALID_MEMO_ID;
const addressDetails = basecoin.getAddressDetails(addressWithMemoId);
addressDetails.address.should.equal(CANTON_ADDRESSES.VALID_ADDRESS);
should.exist(addressDetails.memoId);
addressDetails.memoId!.should.equal('1');
});

it('should throw on multiple memoId query params', function () {
(() => basecoin.getAddressDetails(`${CANTON_ADDRESSES.VALID_ADDRESS}?memoId=1&memoId=2`)).should.throw(
InvalidAddressError
);
});

it('should throw on unknown query params', function () {
(() => basecoin.getAddressDetails(`${CANTON_ADDRESSES.VALID_ADDRESS}?foo=bar`)).should.throw(InvalidAddressError);
});
});
});
Loading