From 063d0ec9ad1978a063a6cc0dd490f8529f2d93dc Mon Sep 17 00:00:00 2001 From: Shuffledex Date: Wed, 27 Nov 2019 14:01:49 -0300 Subject: [PATCH 1/3] feat: add transfer security tokens feature allow transfer security tokens trough transferFromWithData method --- src/procedures/TransferSecurityTokens.ts | 94 ++++++++++++++++++++++++ src/procedures/index.ts | 1 + src/types/index.ts | 12 +++ 3 files changed, 107 insertions(+) create mode 100644 src/procedures/TransferSecurityTokens.ts diff --git a/src/procedures/TransferSecurityTokens.ts b/src/procedures/TransferSecurityTokens.ts new file mode 100644 index 0000000..67757c9 --- /dev/null +++ b/src/procedures/TransferSecurityTokens.ts @@ -0,0 +1,94 @@ +import { BigNumber, TransferStatusCode } from '@polymathnetwork/contract-wrappers'; +import { Procedure } from './Procedure'; +import { + TransferSecurityTokensProcedureArgs, + ErrorCode, + ProcedureType, + PolyTransactionTag, +} from '../types'; +import { PolymathError } from '../PolymathError'; +import { SecurityToken, Shareholder } from '../entities'; +import { Factories } from '~/Context'; + +/** + * Procedure to transfer security tokens. + */ +export class TransferSecurityTokens extends Procedure { + public type = ProcedureType.TransferSecurityTokens; + + private checkTransferStatus( + statusCode: TransferStatusCode, + fromAddress: string, + symbol: string, + to: string, + reasonCode: string + ) { + if (statusCode !== TransferStatusCode.TransferSuccess) { + throw new PolymathError({ + code: ErrorCode.TransferError, + message: `[${statusCode}] ${fromAddress} is not allowed to transfer ${symbol} to ${to}. Possible reason: ${reasonCode}`, + }); + } + } + + public async prepareTransactions() { + const { symbol, to, amount, data = '', from } = this.args; + const { contractWrappers, currentWallet, factories } = this.context; + + let fromAddress = await currentWallet.address(); + + let securityToken; + + try { + securityToken = await contractWrappers.tokenFactory.getSecurityTokenInstanceFromTicker( + symbol + ); + } catch (err) { + throw new PolymathError({ + code: ErrorCode.ProcedureValidationError, + message: `There is no Security Token with symbol ${symbol}`, + }); + } + + if (from && from !== fromAddress) { + fromAddress = from; + const { statusCode, reasonCode } = await securityToken.canTransferFrom({ + to, + value: amount, + from, + }); + this.checkTransferStatus(statusCode, from, symbol, to, reasonCode); + } else { + const { statusCode, reasonCode } = await securityToken.canTransfer({ to, value: amount }); + this.checkTransferStatus(statusCode, fromAddress, symbol, to, reasonCode); + } + + await this.addTransaction(securityToken.transferFromWithData, { + tag: PolyTransactionTag.TransferSecurityTokens, + resolver: createTransferSecurityTokensResolver(factories, symbol, from || fromAddress, to), + })({ from: from || fromAddress, to, value: amount, data }); + } +} + +export const createTransferSecurityTokensResolver = ( + factories: Factories, + symbol: string, + from: string, + to: string +) => async () => { + const refreshingFrom = factories.shareholderFactory.refresh( + Shareholder.generateId({ + securityTokenId: SecurityToken.generateId({ symbol }), + address: from, + }) + ); + + const refreshingTo = factories.shareholderFactory.refresh( + Shareholder.generateId({ + securityTokenId: SecurityToken.generateId({ symbol }), + address: to, + }) + ); + + return Promise.all([refreshingFrom, refreshingTo]); +}; diff --git a/src/procedures/index.ts b/src/procedures/index.ts index 2c18959..68db76d 100644 --- a/src/procedures/index.ts +++ b/src/procedures/index.ts @@ -29,3 +29,4 @@ export { ModifyMaxHolderCount } from './ModifyMaxHolderCount'; export { EnablePercentageTransferManager } from './EnablePercentageTransferManager'; export { ModifyMaxHolderPercentage } from './ModifyMaxHolderPercentage'; export { ModifyPercentageExemptions } from './ModifyPercentageExemptions'; +export { TransferSecurityTokens } from './TransferSecurityTokens'; diff --git a/src/types/index.ts b/src/types/index.ts index b5c2219..55f26ad 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -80,6 +80,7 @@ export enum ErrorCode { InvalidAddress = 'InvalidAddress', InsufficientBalance = 'InsufficientBalance', InexistentModule = 'InexistentModule', + TransferError = 'TransferError', } export interface ShareholderBalance { @@ -129,6 +130,7 @@ export enum ProcedureType { ModifyMaxHolderCount = 'ModifyMaxHolderCount', ModifyMaxHolderPercentage = 'ModifyMaxHolderPercentage', ModifyPercentageExemptions = 'ModifyPercentageExemptions', + TransferSecurityTokens = 'TransferSecurityTokens', } export enum PolyTransactionTag { @@ -167,6 +169,7 @@ export enum PolyTransactionTag { ChangeHolderPercentage = 'ChangeHolderPercentage', ModifyWhitelistMulti = 'ModifyWhitelistMulti', SetAllowPrimaryIssuance = 'SetAllowPrimaryIssuance', + TransferSecurityTokens = 'TransferSecurityTokens', } export type MaybeResolver = PostTransactionResolver | T; @@ -452,6 +455,14 @@ export interface ModifyPercentageExemptionsProcedureArgs { allowPrimaryIssuance?: boolean; } +export interface TransferSecurityTokensProcedureArgs { + symbol: string; + to: string; + amount: BigNumber; + data?: string; + from?: string; +} + export interface ProcedureArguments { [ProcedureType.ApproveErc20]: ApproveErc20ProcedureArgs; [ProcedureType.TransferErc20]: TransferErc20ProcedureArgs; @@ -483,6 +494,7 @@ export interface ProcedureArguments { [ProcedureType.ModifyMaxHolderCount]: ModifyMaxHolderCountProcedureArgs; [ProcedureType.ModifyMaxHolderPercentage]: ModifyMaxHolderPercentageProcedureArgs; [ProcedureType.ModifyPercentageExemptions]: ModifyPercentageExemptionsProcedureArgs; + [ProcedureType.TransferSecurityTokens]: TransferSecurityTokensProcedureArgs; [ProcedureType.UnnamedProcedure]: {}; } From c53160a9a288ada0471bfb1b52ea9509bcba7ee7 Mon Sep 17 00:00:00 2001 From: Shuffledex Date: Thu, 28 Nov 2019 14:02:08 -0300 Subject: [PATCH 2/3] test: add procedure test --- src/procedures/TransferSecurityTokens.ts | 11 +- .../__tests__/TransferSecurityTokens.ts | 178 ++++++++++++++++++ src/types/index.ts | 1 - 3 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 src/procedures/__tests__/TransferSecurityTokens.ts diff --git a/src/procedures/TransferSecurityTokens.ts b/src/procedures/TransferSecurityTokens.ts index 67757c9..200eea0 100644 --- a/src/procedures/TransferSecurityTokens.ts +++ b/src/procedures/TransferSecurityTokens.ts @@ -1,4 +1,4 @@ -import { BigNumber, TransferStatusCode } from '@polymathnetwork/contract-wrappers'; +import { TransferStatusCode } from '@polymathnetwork/contract-wrappers'; import { Procedure } from './Procedure'; import { TransferSecurityTokensProcedureArgs, @@ -8,7 +8,7 @@ import { } from '../types'; import { PolymathError } from '../PolymathError'; import { SecurityToken, Shareholder } from '../entities'; -import { Factories } from '~/Context'; +import { Factories } from '../Context'; /** * Procedure to transfer security tokens. @@ -25,7 +25,7 @@ export class TransferSecurityTokens extends Procedure { + let target: TransferSecurityTokens; + let contextMock: MockManager; + let wrappersMock: MockManager; + let tokenFactoryMock: MockManager; + let securityTokenMock: MockManager; + let shareholderFactoryMock: MockManager; + let factoriesMockedSetup: Factories; + + beforeEach(() => { + contextMock = ImportMock.mockClass(contextModule, 'Context'); + wrappersMock = ImportMock.mockClass(wrappersModule, 'PolymathBase'); + tokenFactoryMock = ImportMock.mockClass(tokenFactoryModule, 'MockedTokenFactoryModule'); + + contextMock.set('contractWrappers', wrappersMock.getMockInstance()); + wrappersMock.set('tokenFactory', tokenFactoryMock.getMockInstance()); + + securityTokenMock = ImportMock.mockClass(contractWrappersModule, 'SecurityToken_3_0_0'); + + tokenFactoryMock.mock( + 'getSecurityTokenInstanceFromTicker', + securityTokenMock.getMockInstance() + ); + + shareholderFactoryMock = ImportMock.mockClass(shareholderFactoryModule, 'ShareholderFactory'); + factoriesMockedSetup = mockFactories(); + factoriesMockedSetup.shareholderFactory = shareholderFactoryMock.getMockInstance(); + contextMock.set('factories', factoriesMockedSetup); + + // Instantiate TransferSecurityTokens + target = new TransferSecurityTokens(params, contextMock.getMockInstance()); + }); + + afterEach(() => { + restore(); + }); + + describe('Types', () => { + test('should extend procedure and have TransferSecurityTokens type', async () => { + expect(target instanceof Procedure).toBe(true); + expect(target.type).toBe(ProcedureType.TransferSecurityTokens); + }); + }); + + describe('TransferSecurityTokens', () => { + test('should throw if there is no valid security token supplied', async () => { + tokenFactoryMock + .mock('getSecurityTokenInstanceFromTicker') + .withArgs(params.symbol) + .throws(); + + await expect(target.prepareTransactions()).rejects.toThrow( + new PolymathError({ + code: ErrorCode.ProcedureValidationError, + message: `There is no Security Token with symbol ${params.symbol}`, + }) + ); + }); + }); + + test('should add a transaction to the queue to execute a transfer security token using a different sender address', async () => { + target = new TransferSecurityTokens( + { ...params, from: '0x1FB52cef867d95E69d398Fe9F6486fAF92C7ED7F' }, + contextMock.getMockInstance() + ); + contextMock.set( + 'currentWallet', + new Wallet({ address: () => Promise.resolve('0x0e6b236a504fce78527497e46dc90c0a6fdc9495') }) + ); + + securityTokenMock.mock( + 'canTransferFrom', + Promise.resolve({ + statusCode: TransferStatusCode.TransferSuccess, + }) + ); + + const addTransactionSpy = spy(target, 'addTransaction'); + securityTokenMock.mock('transferFromWithData', Promise.resolve('TransferFromWithData')); + + await target.prepareTransactions(); + + expect( + addTransactionSpy + .getCall(0) + .calledWith(securityTokenMock.getMockInstance().transferFromWithData) + ).toEqual(true); + expect(addTransactionSpy.getCall(0).lastArg.tag).toEqual( + PolyTransactionTag.TransferSecurityTokens + ); + expect(addTransactionSpy.callCount).toEqual(1); + }); + + test('should throw error if canTransferFrom method returns status code different than success', async () => { + const from = '0x1FB52cef867d95E69d398Fe9F6486fAF92C7ED7F'; + const reasonCode = '0x50'; + + target = new TransferSecurityTokens({ ...params, from }, contextMock.getMockInstance()); + contextMock.set('currentWallet', new Wallet({ address: () => Promise.resolve(from) })); + + securityTokenMock.mock( + 'canTransfer', + Promise.resolve({ + statusCode: TransferStatusCode.TransferFailure, + reasonCode, + }) + ); + + await expect(target.prepareTransactions()).rejects.toThrowError( + new PolymathError({ + code: ErrorCode.ProcedureValidationError, + message: `[${TransferStatusCode.TransferFailure}] ${from} is not allowed to transfer ${ + params.symbol + } to ${params.to}. Possible reason: ${reasonCode}`, + }) + ); + }); + + test('should successfully resolve controller transfer', async () => { + const from = '0x1FB52cef867d95E69d398Fe9F6486fAF92C7ED7F'; + const refreshStub = shareholderFactoryMock.mock('refresh', Promise.resolve()); + const securityTokenId = SecurityToken.generateId({ symbol: params.symbol }); + const resolverValue = await transferSecurityTokensModule.createTransferSecurityTokensResolver( + factoriesMockedSetup, + params.symbol, + '0x1FB52cef867d95E69d398Fe9F6486fAF92C7ED7F', + params.to + )(); + expect( + refreshStub.getCall(0).calledWithExactly( + Shareholder.generateId({ + securityTokenId, + address: from, + }) + ) + ).toEqual(true); + expect( + refreshStub.getCall(1).calledWithExactly( + Shareholder.generateId({ + securityTokenId, + address: params.to, + }) + ) + ).toEqual(true); + expect(resolverValue).toEqual([undefined, undefined]); + expect(refreshStub.callCount).toEqual(2); + }); +}); diff --git a/src/types/index.ts b/src/types/index.ts index 55f26ad..4dbd5b3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -80,7 +80,6 @@ export enum ErrorCode { InvalidAddress = 'InvalidAddress', InsufficientBalance = 'InsufficientBalance', InexistentModule = 'InexistentModule', - TransferError = 'TransferError', } export interface ShareholderBalance { From 189b952b45aa34ac325c1ce916708590c927bddf Mon Sep 17 00:00:00 2001 From: Shuffledex Date: Mon, 9 Dec 2019 10:20:35 -0300 Subject: [PATCH 3/3] fix: typo error --- src/procedures/TransferSecurityTokens.ts | 2 +- src/procedures/__tests__/TransferSecurityTokens.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/procedures/TransferSecurityTokens.ts b/src/procedures/TransferSecurityTokens.ts index 200eea0..d381d22 100644 --- a/src/procedures/TransferSecurityTokens.ts +++ b/src/procedures/TransferSecurityTokens.ts @@ -64,7 +64,7 @@ export class TransferSecurityTokens extends Procedure { ); }); - test('should successfully resolve controller transfer', async () => { + test('should successfully refresh the corresponding balance of each shareholder involved', async () => { const from = '0x1FB52cef867d95E69d398Fe9F6486fAF92C7ED7F'; const refreshStub = shareholderFactoryMock.mock('refresh', Promise.resolve()); const securityTokenId = SecurityToken.generateId({ symbol: params.symbol }); const resolverValue = await transferSecurityTokensModule.createTransferSecurityTokensResolver( factoriesMockedSetup, params.symbol, - '0x1FB52cef867d95E69d398Fe9F6486fAF92C7ED7F', + from, params.to )(); expect(