Skip to content
Merged
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
54 changes: 53 additions & 1 deletion modules/sdk-coin-canton/src/lib/cantonCommandBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import {
TransactionType,
CantonCommand,
CantonCommandResolveContractSpec,
CantonExerciseCommand,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { CANTON_COMMAND_KEYS, CantonCommandRequest, CantonPrepareCommandResponse } from './iface';
import {
CANTON_COMMAND_KEYS,
CantonAllowListCommandParams,
CantonCommandRequest,
CantonPrepareCommandResponse,
} from './iface';
import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from './transaction/transaction';
import utils from './utils';
Expand Down Expand Up @@ -127,6 +133,52 @@ export class CantonCommandBuilder extends TransactionBuilder {
return this;
}

/**
* Sets the V3 participant allow-list add choice.
*
* The DAML choice is exercised on ParticipantService, while allowListCid and
* credentialCids are supplied as the choice argument.
*/
addWalletsToAllowList(params: CantonAllowListCommandParams): this {
return this.setAllowListCommand(params, 'ParticipantService_AddWalletsToAllowList');
}

/** Sets the V3 participant allow-list remove choice. */
removeWalletsFromAllowList(params: CantonAllowListCommandParams): this {
return this.setAllowListCommand(params, 'ParticipantService_RemoveWalletsFromAllowList');
}

private setAllowListCommand(
params: CantonAllowListCommandParams,
choice: 'ParticipantService_AddWalletsToAllowList' | 'ParticipantService_RemoveWalletsFromAllowList'
): this {
const templateId = params?.templateId?.trim();
const participantServiceCid = params?.participantServiceCid?.trim();
const allowListCid = params?.allowListCid?.trim();
if (!templateId) throw new Error('templateId must be a non-empty string');
if (!participantServiceCid) throw new Error('participantServiceCid must be a non-empty string');
if (!allowListCid) throw new Error('allowListCid must be a non-empty string');
if (!Array.isArray(params.credentialCids)) throw new Error('credentialCids must be an array');

const credentialCids = params.credentialCids.map((credentialCid) => credentialCid?.trim());
if (credentialCids.some((credentialCid) => !credentialCid)) {
throw new Error('credentialCids must contain only non-empty strings');
}
if (new Set(credentialCids).size !== credentialCids.length) {
throw new Error('credentialCids must not contain duplicates');
}

const command: CantonExerciseCommand = {
ExerciseCommand: {
templateId,
contractId: participantServiceCid,
choice,
choiceArgument: { allowListCid, credentialCids },
},
};
return this.command(command);
}

/**
* Sets the list of ACS contract resolution specs that IMS will resolve before prepare.
*
Expand Down
10 changes: 10 additions & 0 deletions modules/sdk-coin-canton/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,16 @@ export interface CantonCommandRequest {
resolveContracts?: CantonCommandResolveContractSpec[];
token?: string;
}
export interface CantonAllowListCommandParams {
/** Fully-qualified V3 ParticipantService template ID, including package reference. */
templateId: string;
/** ParticipantService contract being exercised. */
participantServiceCid: string;
/** Participant-owned AllowList contract passed to the choice. */
allowListCid: string;
/** End-investor Credential contracts. Empty is valid and preserves DAR no-op semantics. */
credentialCids: string[];
}

// Root command decoded from the prepared Canton transaction protobuf, used during verifyTransaction.
export interface CantonCommandInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { coins } from '@bitgo/statics';
import { CantonCommandBuilder } from '../../../../src/lib/cantonCommandBuilder';
import { TransactionBuilderFactory } from '../../../../src/lib/transactionBuilderFactory';
import { Transaction } from '../../../../src/lib/transaction/transaction';
import { CantonCommandRequest } from '../../../../src/lib/iface';
import { CantonAllowListCommandParams, CantonCommandRequest } from '../../../../src/lib/iface';
import { CantonExerciseCommandPrepareResponse } from '../../../resources';

const PARTY_A = 'RegRegistrar::1220508e54fb709dcb1a9b4096408ad415d2217d8881ad9034a9850752933ba88ad5';
Expand Down Expand Up @@ -137,7 +137,81 @@ describe('CantonCommandBuilder', () => {
assert.throws(() => builder.command(null as unknown as CantonExerciseCommand), /command must be a plain object/);
});
});
describe('participant allow-list choices', () => {
const allowListParams: CantonAllowListCommandParams = {
templateId: TEMPLATE_ID,
participantServiceCid: CONTRACT_ID,
allowListCid: 'allow-list-cid',
credentialCids: ['credential-a', 'credential-b'],
};

it('builds the V3 add choice with the ParticipantService contract', () => {
const builder = new CantonCommandBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
builder.initBuilder(tx);
builder.commandId('cmd-allow-add').actAs([PARTY_A]).addWalletsToAllowList(allowListParams);

const request = builder.toRequestObject();
assert.deepEqual(request.command, {
ExerciseCommand: {
templateId: TEMPLATE_ID,
contractId: CONTRACT_ID,
choice: 'ParticipantService_AddWalletsToAllowList',
choiceArgument: {
allowListCid: 'allow-list-cid',
credentialCids: ['credential-a', 'credential-b'],
},
},
});
});

it('builds the V3 remove choice and preserves an empty credential list', () => {
const builder = new CantonCommandBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
builder.initBuilder(tx);
builder
.commandId('cmd-allow-remove')
.actAs([PARTY_A])
.removeWalletsFromAllowList({
...allowListParams,
credentialCids: [],
});

const request = builder.toRequestObject();
assert.equal(
(request.command as CantonExerciseCommand).ExerciseCommand.choice,
'ParticipantService_RemoveWalletsFromAllowList'
);
assert.deepEqual((request.command as CantonExerciseCommand).ExerciseCommand.choiceArgument, {
allowListCid: 'allow-list-cid',
credentialCids: [],
});
});

it('rejects duplicate credential IDs', () => {
const builder = new CantonCommandBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
builder.initBuilder(tx);
assert.throws(
() =>
builder.addWalletsToAllowList({
...allowListParams,
credentialCids: ['credential-a', 'credential-a'],
}),
/credentialCids must not contain duplicates/
);
});

it('rejects missing contract identifiers', () => {
const builder = new CantonCommandBuilder(coins.get('tcanton'));
const tx = new Transaction(coins.get('tcanton'));
builder.initBuilder(tx);
assert.throws(
() => builder.addWalletsToAllowList({ ...allowListParams, allowListCid: ' ' }),
/allowListCid must be a non-empty string/
);
});
});
describe('token()', () => {
it('should set the token', function () {
const builder = new CantonCommandBuilder(coins.get('tcanton'));
Expand Down
Loading