Skip to content

Commit

Permalink
Merge pull request #150 from PolymathNetwork/feat/transfer-ticker-own…
Browse files Browse the repository at this point in the history
…ership

feat: transfer ticker ownership procedure
  • Loading branch information
monitz87 committed Dec 23, 2019
2 parents 127ee9b + 64aa648 commit de3db30
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 1 deletion.
13 changes: 12 additions & 1 deletion src/entities/SecurityTokenReservation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Entity } from './Entity';
import { serialize, unserialize } from '../utils';
import { Context } from '../Context';
import { CreateSecurityToken } from '../procedures';
import { CreateSecurityToken, TransferReservationOwnership } from '../procedures';
import { PolymathError } from '../PolymathError';
import { ErrorCode } from '../types';

Expand Down Expand Up @@ -115,6 +115,17 @@ export class SecurityTokenReservation extends Entity<Params> {
return context.contractWrappers.securityTokenRegistry.isTokenLaunched({ ticker: symbol });
};

/**
* Transfer the ownership of the ticker
*/
public transferOwnership = async (args: { newOwner: string }) => {
const { context, symbol } = this;
const { newOwner } = args;
const procedure = new TransferReservationOwnership({ symbol, newOwner }, context);

return procedure.prepare();
};

public toPojo() {
const { uid, symbol, expiry, securityTokenAddress, reservedAt, ownerAddress } = this;

Expand Down
66 changes: 66 additions & 0 deletions src/procedures/TransferReservationOwnership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Procedure } from './Procedure';
import {
ProcedureType,
PolyTransactionTag,
TransferReservationOwnershipProcedureArgs,
ErrorCode,
} from '../types';
import { PolymathError } from '../PolymathError';
import { SecurityTokenReservation } from '../entities';
import { Factories } from '../Context';

export class TransferReservationOwnership extends Procedure<
TransferReservationOwnershipProcedureArgs
> {
public type = ProcedureType.TransferReservationOwnership;

public async prepareTransactions() {
const { newOwner, symbol } = this.args;
const {
contractWrappers: { securityTokenRegistry },
currentWallet,
factories,
} = this.context;

const { status, owner } = await securityTokenRegistry.getTickerDetails({
ticker: symbol,
});

const account = await currentWallet.address();

if (status) {
throw new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: `The ${symbol} Security Token has already been launched, ownership cannot be transferred`,
});
}

if (account !== owner) {
throw new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: `Only the reservation owner can transfer ownership to another wallet`,
});
}

if (newOwner === owner) {
throw new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: `New owner must be different from the current one to transfer ownership`,
});
}

await this.addTransaction(securityTokenRegistry.transferTickerOwnership, {
tag: PolyTransactionTag.TransferReservationOwnership,
resolvers: [createTransferReservationOwnershipResolver(factories, symbol)],
})({ newOwner, ticker: symbol });
}
}

export const createTransferReservationOwnershipResolver = (
factories: Factories,
symbol: string
) => async () => {
return factories.securityTokenReservationFactory.refresh(
SecurityTokenReservation.generateId({ symbol })
);
};
172 changes: 172 additions & 0 deletions src/procedures/__tests__/TransferReservationOwnership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { ImportMock, MockManager } from 'ts-mock-imports';
import { spy, restore } from 'sinon';
import * as contractWrappersModule from '@polymathnetwork/contract-wrappers';
import * as contextModule from '../../Context';
import * as wrappersModule from '../../PolymathBase';
import * as tokenFactoryModule from '../../testUtils/MockedTokenFactoryModule';
import { TransferReservationOwnership } from '../../procedures/TransferReservationOwnership';
import { Procedure } from '../../procedures/Procedure';
import { ProcedureType, PolyTransactionTag, ErrorCode } from '../../types';
import { PolymathError } from '../../PolymathError';
import * as TransferReservationOwnershipModule from '../../procedures/TransferReservationOwnership';
import * as securityTokenReservationFactoryModule from '../../entities/factories/SecurityTokenReservationFactory';
import { mockFactories } from '../../testUtils/mockFactories';
import { Wallet } from '../../Wallet';
import { Factories } from '../../Context';
import { SecurityTokenReservation } from '../../entities';

const params = {
symbol: 'TEST',
newOwner: '0x3333333333333333333333333333333333333333',
};

describe('TransferReservationOwnership', () => {
let target: TransferReservationOwnership;
let contextMock: MockManager<contextModule.Context>;
let wrappersMock: MockManager<wrappersModule.PolymathBase>;
let tokenFactoryMock: MockManager<tokenFactoryModule.MockedTokenFactoryModule>;
let securityTokenMock: MockManager<contractWrappersModule.SecurityToken_3_0_0>;
let securityTokenRegistryMock: MockManager<contractWrappersModule.SecurityTokenRegistry>;
let securityTokenReservationFactoryMock: MockManager<
securityTokenReservationFactoryModule.SecurityTokenReservationFactory
>;
let factoryMockSetup: Factories;

beforeEach(() => {
// Mock the context, wrappers, tokenFactory and securityToken to test TransferReservationOwnership
contextMock = ImportMock.mockClass(contextModule, 'Context');
wrappersMock = ImportMock.mockClass(wrappersModule, 'PolymathBase');
tokenFactoryMock = ImportMock.mockClass(tokenFactoryModule, 'MockedTokenFactoryModule');
securityTokenMock = ImportMock.mockClass(contractWrappersModule, 'SecurityToken_3_0_0');
securityTokenRegistryMock = ImportMock.mockClass(
contractWrappersModule,
'SecurityTokenRegistry'
);
securityTokenReservationFactoryMock = ImportMock.mockClass(
securityTokenReservationFactoryModule,
'SecurityTokenReservationFactory'
);

securityTokenRegistryMock.mock(
'getTickerDetails',
Promise.resolve({ status: false, owner: '0x01' })
);

tokenFactoryMock.mock(
'getSecurityTokenInstanceFromTicker',
securityTokenMock.getMockInstance()
);

factoryMockSetup = mockFactories();
factoryMockSetup.securityTokenReservationFactory = securityTokenReservationFactoryMock.getMockInstance();

wrappersMock.set('tokenFactory', tokenFactoryMock.getMockInstance());
wrappersMock.set('securityTokenRegistry', securityTokenRegistryMock.getMockInstance());
contextMock.set('contractWrappers', wrappersMock.getMockInstance());
contextMock.set('currentWallet', new Wallet({ address: () => Promise.resolve('0x02') }));
contextMock.set('factories', factoryMockSetup);

// Instantiate TransferReservationOwnership
target = new TransferReservationOwnership(params, contextMock.getMockInstance());
});

afterEach(() => {
restore();
});

describe('Types', () => {
test('should extend procedure and have TransferReservationOwnership type', async () => {
expect(target instanceof Procedure).toBe(true);
expect(target.type).toBe(ProcedureType.TransferReservationOwnership);
});
});

describe('TransferReservationOwnership', () => {
test('should throw error if token is already launched', async () => {
securityTokenRegistryMock.mock(
'getTickerDetails',
Promise.resolve({ status: true, owner: '0x01' })
);

await expect(target.prepareTransactions()).rejects.toThrowError(
new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: `The ${
params.symbol
} Security Token has already been launched, ownership cannot be transferred`,
})
);
});

test('should throw error if current wallet is not the reservation owner', async () => {
await expect(target.prepareTransactions()).rejects.toThrowError(
new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: `Only the reservation owner can transfer ownership to another wallet`,
})
);
});

test('should throw error if new owner is equal to the current one', async () => {
contextMock.set('currentWallet', new Wallet({ address: () => Promise.resolve('0x01') }));

target = new TransferReservationOwnership(
{ ...params, newOwner: '0x01' },
contextMock.getMockInstance()
);

await expect(target.prepareTransactions()).rejects.toThrowError(
new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: `New owner must be different from the current one to transfer ownership`,
})
);
});

test('should add a transaction to the queue to transfer ownership of the reservation', async () => {
contextMock.set('currentWallet', new Wallet({ address: () => Promise.resolve('0x01') }));

const addTransactionSpy = spy(target, 'addTransaction');
securityTokenRegistryMock.mock(
'transferTickerOwnership',
Promise.resolve('TransferTickerOwnership')
);

// Real call
await target.prepareTransactions();

// Verifications
expect(
addTransactionSpy
.getCall(0)
.calledWith(securityTokenRegistryMock.getMockInstance().transferTickerOwnership)
).toEqual(true);
expect(addTransactionSpy.getCall(0).lastArg.tag).toEqual(
PolyTransactionTag.TransferReservationOwnership
);
expect(addTransactionSpy.callCount).toEqual(1);
});

test('should successfully refresh the security token reservation with the new owner address', async () => {
const refreshStub = securityTokenReservationFactoryMock.mock(
'refresh',
Promise.resolve(undefined)
);

const resolverValue = await TransferReservationOwnershipModule.createTransferReservationOwnershipResolver(
factoryMockSetup,
params.symbol
)();

expect(
refreshStub.getCall(0).calledWithExactly(
SecurityTokenReservation.generateId({
symbol: params.symbol,
})
)
).toEqual(true);
expect(resolverValue).toEqual(undefined);
expect(refreshStub.callCount).toEqual(1);
});
});
});
1 change: 1 addition & 0 deletions src/procedures/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ export { TransferSecurityTokens } from './TransferSecurityTokens';
export { ToggleFreezeTransfers } from './ToggleFreezeTransfers';
export { ModifyDividendsDefaultExclusionList } from './ModifyDividendsDefaultExclusionList';
export { ModifyPreIssuing } from './ModifyPreIssuing';
export { TransferReservationOwnership } from './TransferReservationOwnership';
8 changes: 8 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export enum ProcedureType {
ModifyPercentageExemptions = 'ModifyPercentageExemptions',
TransferSecurityTokens = 'TransferSecurityTokens',
ToggleFreezeTransfers = 'ToggleFreezeTransfers',
TransferReservationOwnership = 'TransferReservationOwnershipProcedureArgs',
}

export enum PolyTransactionTag {
Expand Down Expand Up @@ -227,6 +228,7 @@ export enum PolyTransactionTag {
UnfreezeTransfers = 'UnfreezeTransfers',
FreezeTransfers = 'FreezeTransfers',
Signature = 'Signature',
TransferReservationOwnership = 'TransferReservationOwnership',
}

export type MaybeResolver<T> = PostTransactionResolver<T, any> | T;
Expand Down Expand Up @@ -545,6 +547,11 @@ export interface DisableControllerProcedureArgs {
signature?: string;
}

export interface TransferReservationOwnershipProcedureArgs {
symbol: string;
newOwner: string;
}

export interface ShareholderDataEntry {
/**
* shareholder wallet address to whitelist
Expand Down Expand Up @@ -659,6 +666,7 @@ export interface ProcedureArguments {
[ProcedureType.ModifyPercentageExemptions]: ModifyPercentageExemptionsProcedureArgs;
[ProcedureType.TransferSecurityTokens]: TransferSecurityTokensProcedureArgs;
[ProcedureType.ToggleFreezeTransfers]: ToggleFreezeTransfersProcedureArgs;
[ProcedureType.TransferReservationOwnership]: TransferReservationOwnershipProcedureArgs;
[ProcedureType.UnnamedProcedure]: {};
}

Expand Down

0 comments on commit de3db30

Please sign in to comment.