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
23 changes: 17 additions & 6 deletions src/procedures/TransferErc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Procedure } from './Procedure';
import { TransferErc20ProcedureArgs, ErrorCode, ProcedureType, PolyTransactionTag } from '../types';
import { PolymathError } from '../PolymathError';
import { Erc20TokenBalance } from '../entities';
import { Factories } from '~/Context';

/**
* Procedure to transfer funds of an ERC20 token. If no token address is specified, it defaults to POLY
Expand Down Expand Up @@ -34,7 +35,9 @@ export class TransferErc20 extends Procedure<TransferErc20ProcedureArgs> {
}

try {
token = await contractWrappers.getERC20TokenWrapper({ address: tokenAddress });
token = await contractWrappers.getERC20TokenWrapper({
address: tokenAddress,
});
} catch (err) {
throw new PolymathError({
code: ErrorCode.ProcedureValidationError,
Expand Down Expand Up @@ -70,11 +73,19 @@ export class TransferErc20 extends Procedure<TransferErc20ProcedureArgs> {

await this.addTransaction(token.transfer, {
tag: PolyTransactionTag.TransferErc20,
resolver: async _receipt => {
return factories.erc20TokenBalanceFactory.refresh(
Erc20TokenBalance.generateId({ tokenAddress: address, walletAddress: receiver })
);
},
resolver: createTransferErc20Resolver(factories, address, receiver),
})({ to: receiver, value: amount });
}
}
export const createTransferErc20Resolver = (
factories: Factories,
tokenAddress: string,
receiver: string
) => async () => {
return factories.erc20TokenBalanceFactory.refresh(
Erc20TokenBalance.generateId({
tokenAddress,
walletAddress: receiver,
})
);
};
1 change: 1 addition & 0 deletions src/procedures/__tests__/AssignSecurityTokenRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ describe('AssignSecurityTokenRole', () => {
// Mock the context, wrappers, and tokenFactory to test AssignSecurityTokenRole
contextMock = ImportMock.mockClass(contextModule, 'Context');
wrappersMock = ImportMock.mockClass(wrappersModule, 'PolymathBase');

tokenFactoryMock = ImportMock.mockClass(tokenFactoryModule, 'MockedTokenFactoryModule');

contextMock.set('contractWrappers', wrappersMock.getMockInstance());
Expand Down
223 changes: 223 additions & 0 deletions src/procedures/__tests__/TransferErc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { ImportMock, MockManager } from 'ts-mock-imports';
import { restore, spy } from 'sinon';
import * as contractWrappersModule from '@polymathnetwork/contract-wrappers';
import { BigNumber, TransactionReceiptWithDecodedLogs } from '@polymathnetwork/contract-wrappers';
import { Procedure } from '../Procedure';
import {
ErrorCode,
PolyTransactionTag,
ProcedureType,
TransferErc20ProcedureArgs,
} from '../../types';
import * as erc20TokenBalanceFactoryModule from '../../entities/factories/Erc20TokenBalanceFactory';
import * as contextModule from '../../Context';
import * as wrappersModule from '../../PolymathBase';
import * as tokenFactoryModule from '../../testUtils/MockedTokenFactoryModule';
import * as moduleWrapperFactoryModule from '../../testUtils/MockedModuleWrapperFactoryModule';
import { Wallet } from '../../Wallet';
import { TransferErc20 } from '../../procedures';
import * as transferErc20Module from '../../procedures/TransferErc20';
import { mockFactories } from '../../testUtils/mockFactories';
import { PolymathError } from '../../PolymathError';
import { Erc20TokenBalance } from '../../entities';
import { Factories } from '../../Context';

const params: TransferErc20ProcedureArgs = {
amount: new BigNumber(10),
receiver: '0x6666666666666666666666666666666666666666',
tokenAddress: '0x7777777777777777777777777777777777777777',
};
const currentWallet = '0x8888888888888888888888888888888888888888';
const polyTokenAddress = '0x9999999999999999999999999999999999999999';

describe('TransferErc20', () => {
let target: TransferErc20;
let contextMock: MockManager<contextModule.Context>;
let wrappersMock: MockManager<wrappersModule.PolymathBase>;
let tokenFactoryMock: MockManager<tokenFactoryModule.MockedTokenFactoryModule>;
let moduleWrapperFactoryMock: MockManager<
moduleWrapperFactoryModule.MockedModuleWrapperFactoryModule
>;
let polyTokenMock: MockManager<contractWrappersModule.PolyToken>;

// Mock factories
let erc20TokenBalanceFactoryMock: MockManager<
erc20TokenBalanceFactoryModule.Erc20TokenBalanceFactory
>;

let securityTokenRegistryMock: MockManager<contractWrappersModule.SecurityTokenRegistry>;

let erc20Mock: MockManager<contractWrappersModule.ERC20>;
let factoryMockSetup: Factories;

beforeEach(() => {
// Mock the context, wrappers, and tokenFactory to test TransferErc20
contextMock = ImportMock.mockClass(contextModule, 'Context');
wrappersMock = ImportMock.mockClass(wrappersModule, 'PolymathBase');
tokenFactoryMock = ImportMock.mockClass(tokenFactoryModule, 'MockedTokenFactoryModule');
moduleWrapperFactoryMock = ImportMock.mockClass(
moduleWrapperFactoryModule,
'MockedModuleWrapperFactoryModule'
);

contextMock.set('contractWrappers', wrappersMock.getMockInstance());
wrappersMock.set('tokenFactory', tokenFactoryMock.getMockInstance());
wrappersMock.set('moduleFactory', moduleWrapperFactoryMock.getMockInstance());

erc20Mock = ImportMock.mockClass(contractWrappersModule, 'ERC20');
erc20Mock.mock('balanceOf', Promise.resolve(new BigNumber(20)));

erc20Mock.mock('address', Promise.resolve(params.tokenAddress));

securityTokenRegistryMock = ImportMock.mockClass(
contractWrappersModule,
'SecurityTokenRegistry'
);
securityTokenRegistryMock.mock('isSecurityToken', Promise.resolve(false));

wrappersMock.set('securityTokenRegistry', securityTokenRegistryMock.getMockInstance());
wrappersMock.mock('getERC20TokenWrapper', erc20Mock.getMockInstance());

erc20TokenBalanceFactoryMock = ImportMock.mockClass(
erc20TokenBalanceFactoryModule,
'Erc20TokenBalanceFactory'
);

factoryMockSetup = mockFactories();
factoryMockSetup.erc20TokenBalanceFactory = erc20TokenBalanceFactoryMock.getMockInstance();

erc20TokenBalanceFactoryMock.mock('refresh', Promise.resolve());
contextMock.set('factories', factoryMockSetup);
contextMock.set('currentWallet', new Wallet({ address: () => Promise.resolve(currentWallet) }));

polyTokenMock = ImportMock.mockClass(contractWrappersModule, 'PolyToken');
polyTokenMock.mock('address', Promise.resolve(polyTokenAddress));
wrappersMock.set('polyToken', polyTokenMock.getMockInstance());
wrappersMock.mock('isTestnet', Promise.resolve(false));

// Instantiate TransferErc20
target = new TransferErc20(params, contextMock.getMockInstance());
});
afterEach(() => {
restore();
});

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

describe('TransferErc20', () => {
test('should add a transaction to the queue to transfer an erc20 token with specified token address to a specified receiving address', async () => {
const addTransactionSpy = spy(target, 'addTransaction');
erc20Mock.mock('transfer', Promise.resolve('Transfer'));
// Real call
await target.prepareTransactions();

// Verifications
expect(addTransactionSpy.getCall(0).calledWith(erc20Mock.getMockInstance().transfer)).toEqual(
true
);
expect(addTransactionSpy.getCall(0).lastArg.tag).toEqual(PolyTransactionTag.TransferErc20);
expect(addTransactionSpy.callCount).toEqual(1);
});

test('should add a transaction to the queue to transfer poly as the parameters do not include token address', async () => {
target = new TransferErc20(
{ ...params, tokenAddress: undefined },
contextMock.getMockInstance()
);
polyTokenMock.mock('balanceOf', Promise.resolve(new BigNumber(20)));

const addTransactionSpy = spy(target, 'addTransaction');
polyTokenMock.mock('transfer', Promise.resolve('Transfer'));

// Real call
await target.prepareTransactions();

// Verifications
expect(
addTransactionSpy.getCall(0).calledWith(polyTokenMock.getMockInstance().transfer)
).toEqual(true);
expect(addTransactionSpy.getCall(0).lastArg.tag).toEqual(PolyTransactionTag.TransferErc20);
expect(addTransactionSpy.callCount).toEqual(1);
});

test('should throw if supplied address does not correspond to a valid erc20 token', async () => {
wrappersMock
.mock('getERC20TokenWrapper')
.withArgs({ address: params.tokenAddress })
.throws();

await expect(target.prepareTransactions()).rejects.toThrow(
new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: 'The supplied address does not correspond to an ERC20 token',
})
);
});

test('should throw if address belongs to a security token, not an erc20 token', async () => {
securityTokenRegistryMock.mock('isSecurityToken', Promise.resolve(true));

await expect(target.prepareTransactions()).rejects.toThrow(
new PolymathError({
code: ErrorCode.ProcedureValidationError,
message:
"This address belongs to a Security Token. To transfer Security Tokens, use the functions in the Security Token's transfers namespace",
})
);
});

test('should add an extra transaction to get POLY from the faucet if the balance is insufficient, specifically on testnet', async () => {
wrappersMock.mock('isTestnet', Promise.resolve(true));
erc20Mock.mock('address', Promise.resolve(polyTokenAddress));
erc20Mock.mock('balanceOf', Promise.resolve(new BigNumber(2)));
erc20Mock.mock('transfer', Promise.resolve('Transfer'));
wrappersMock.mock('getPolyTokens', Promise.resolve('GetPolyTokens'));
const addTransactionSpy = spy(target, 'addTransaction');
// Real call
await target.prepareTransactions();

// Verifications
expect(
addTransactionSpy.getCall(0).calledWith(wrappersMock.getMockInstance().getPolyTokens)
).toEqual(true);
expect(addTransactionSpy.getCall(0).lastArg.tag).toEqual(PolyTransactionTag.GetTokens);
expect(addTransactionSpy.getCall(1).calledWith(erc20Mock.getMockInstance().transfer)).toEqual(
true
);
expect(addTransactionSpy.getCall(1).lastArg.tag).toEqual(PolyTransactionTag.TransferErc20);
expect(addTransactionSpy.callCount).toEqual(2);
});

test('should throw error if there are not enough funds to make an erc20 transfer', async () => {
erc20Mock.mock('balanceOf', Promise.resolve(new BigNumber(2)));

await expect(target.prepareTransactions()).rejects.toThrow(
new PolymathError({
code: ErrorCode.ProcedureValidationError,
message: 'Not enough funds',
})
);
});

test('should successfully refresh the corresponding ERC20 Balance Entity', async () => {
const refreshStub = erc20TokenBalanceFactoryMock.mock('refresh', Promise.resolve());
const erc20TokenBalanceGeneratedId = Erc20TokenBalance.generateId({
tokenAddress: params.tokenAddress!,
walletAddress: params.receiver,
});
const resolverValue = await transferErc20Module.createTransferErc20Resolver(
factoryMockSetup,
params.tokenAddress!,
params.receiver
)();
expect(refreshStub.getCall(0).calledWithExactly(erc20TokenBalanceGeneratedId)).toEqual(true);
expect(resolverValue).toEqual(undefined);
expect(refreshStub.callCount).toEqual(1);
});
});
});