From 0e8db92aeab8f119ee9f75276db945d30e973ff7 Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Fri, 10 Mar 2023 18:19:03 +0100 Subject: [PATCH 1/6] refactor: change client signer validation logic --- package-lock.json | 2 +- package.json | 2 +- src/Caller/CallerClient.ts | 7 +++-- src/DripsHub/DripsHubClient.ts | 8 +++--- ...iver.ts => ImmutableSplitsDriverClient.ts} | 7 +++-- src/NFTDriver/NFTDriverClient.ts | 7 +++-- src/common/DripsError.ts | 7 ++--- src/common/validators.ts | 28 ++++++++++++++----- src/index.ts | 2 +- tests/Caller/CallerClient.tests.ts | 6 ++-- tests/DripsHub/DripsHubClient.tests.ts | 6 ++-- ...bleSplitsDriverClient.integration.tests.ts | 2 +- .../ImmutableSplitsDriverClient.tests.ts | 8 +++--- tests/NFTDriver/NFTDriverClient.tests.ts | 10 +++---- tests/common/DripsError.tests.ts | 6 ++-- tests/common/validators.tests.ts | 8 +++--- 16 files changed, 66 insertions(+), 50 deletions(-) rename src/ImmutableSplits/{ImmutableSplitsDriver.ts => ImmutableSplitsDriverClient.ts} (95%) diff --git a/package-lock.json b/package-lock.json index 5f9f086f..c59ad089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", - "ethers": "^5.6.2", + "ethers": "^5.7.2", "graphql": "^16.6.0", "mocha": "^10.0.0", "nodemon": "^2.0.16", diff --git a/package.json b/package.json index afe6d568..3c62ff80 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", - "ethers": "^5.6.2", + "ethers": "^5.7.2", "graphql": "^16.6.0", "mocha": "^10.0.0", "nodemon": "^2.0.16", diff --git a/src/Caller/CallerClient.ts b/src/Caller/CallerClient.ts index c0b76aa6..80ce3c00 100644 --- a/src/Caller/CallerClient.ts +++ b/src/Caller/CallerClient.ts @@ -51,18 +51,19 @@ export default class CallerClient { * @param {string|undefined} customCallerAddress Overrides the `NFTDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. - * @throws {@link DripsErrors.clientInitializationError} if the client initialization fails. + * @throws {@link DripsErrors.initializationError} if the client initialization fails. */ public static async create(provider: Provider, signer: Signer, customCallerAddress?: string): Promise { try { await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); - await validateClientSigner(signer); if (!signer.provider) { // eslint-disable-next-line no-param-reassign signer = signer.connect(provider); } + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); + const network = await provider.getNetwork(); const callerAddress = customCallerAddress ?? Utils.Network.configs[network.chainId].CONTRACT_CALLER; @@ -75,7 +76,7 @@ export default class CallerClient { return client; } catch (error: any) { - throw DripsErrors.clientInitializationError(`Could not create 'CallerClient': ${error.message}`); + throw DripsErrors.initializationError(`Could not create 'CallerClient': ${error.message}`); } } diff --git a/src/DripsHub/DripsHubClient.ts b/src/DripsHub/DripsHubClient.ts index 7f3af7eb..0af53c09 100644 --- a/src/DripsHub/DripsHubClient.ts +++ b/src/DripsHub/DripsHubClient.ts @@ -65,7 +65,7 @@ export default class DripsHubClient { * @param {string|undefined} customDriverAddress Overrides the `NFTDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. - * @throws {@link DripsErrors.clientInitializationError} if the client initialization fails. + * @throws {@link DripsErrors.initializationError} if the client initialization fails. */ public static async create( provider: Provider, @@ -76,12 +76,12 @@ export default class DripsHubClient { await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); if (signer) { - await validateClientSigner(signer); - if (!signer.provider) { // eslint-disable-next-line no-param-reassign signer = signer.connect(provider); } + + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); } const network = await provider.getNetwork(); @@ -95,7 +95,7 @@ export default class DripsHubClient { client.#driver = DripsHub__factory.connect(contractAddress, signer ?? provider); return client; } catch (error: any) { - throw DripsErrors.clientInitializationError(`Could not create 'DripsHubClient': ${error.message}`); + throw DripsErrors.initializationError(`Could not create 'DripsHubClient': ${error.message}`); } } diff --git a/src/ImmutableSplits/ImmutableSplitsDriver.ts b/src/ImmutableSplits/ImmutableSplitsDriverClient.ts similarity index 95% rename from src/ImmutableSplits/ImmutableSplitsDriver.ts rename to src/ImmutableSplits/ImmutableSplitsDriverClient.ts index f6fc5320..d3912b78 100644 --- a/src/ImmutableSplits/ImmutableSplitsDriver.ts +++ b/src/ImmutableSplits/ImmutableSplitsDriverClient.ts @@ -61,7 +61,7 @@ export default class ImmutableSplitsDriverClient { * @param {string|undefined} customDriverAddress Overrides the `NFTDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. - * @throws {@link DripsErrors.clientInitializationError} if the client initialization fails. + * @throws {@link DripsErrors.initializationError} if the client initialization fails. */ public static async create( provider: Provider, @@ -70,13 +70,14 @@ export default class ImmutableSplitsDriverClient { ): Promise { try { await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); - await validateClientSigner(signer); if (!signer.provider) { // eslint-disable-next-line no-param-reassign signer = signer.connect(provider); } + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); + const network = await provider.getNetwork(); const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_IMMUTABLE_SPLITS_DRIVER; @@ -90,7 +91,7 @@ export default class ImmutableSplitsDriverClient { return client; } catch (error: any) { - throw DripsErrors.clientInitializationError(`Could not create 'ImmutableSplitsDriverClient': ${error.message}`); + throw DripsErrors.initializationError(`Could not create 'ImmutableSplitsDriverClient': ${error.message}`); } } diff --git a/src/NFTDriver/NFTDriverClient.ts b/src/NFTDriver/NFTDriverClient.ts index 4f32c40d..2f941682 100644 --- a/src/NFTDriver/NFTDriverClient.ts +++ b/src/NFTDriver/NFTDriverClient.ts @@ -68,7 +68,7 @@ export default class NFTDriverClient { * @param {string|undefined} customDriverAddress Overrides the `NFTDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. - * @throws {@link DripsErrors.clientInitializationError} if the client initialization fails. + * @throws {@link DripsErrors.initializationError} if the client initialization fails. */ public static async create( provider: Provider, @@ -77,13 +77,14 @@ export default class NFTDriverClient { ): Promise { try { await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); - await validateClientSigner(signer); if (!signer.provider) { // eslint-disable-next-line no-param-reassign signer = signer.connect(provider); } + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); + const network = await provider.getNetwork(); const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_NFT_DRIVER; @@ -97,7 +98,7 @@ export default class NFTDriverClient { return client; } catch (error: any) { - throw DripsErrors.clientInitializationError(`Could not create 'NFTDriverClient': ${error.message}`); + throw DripsErrors.initializationError(`Could not create 'NFTDriverClient': ${error.message}`); } } diff --git a/src/common/DripsError.ts b/src/common/DripsError.ts index fd012a88..a0a47303 100644 --- a/src/common/DripsError.ts +++ b/src/common/DripsError.ts @@ -11,8 +11,8 @@ export enum DripsErrorCode { UNSUPPORTED_NETWORK = 'UNSUPPORTED_NETWORK', SUBGRAPH_QUERY_ERROR = 'SUBGRAPH_QUERY_ERROR', INVALID_DRIPS_RECEIVER = 'INVALID_DRIPS_RECEIVER', + INITIALIZATION_FAILURE = 'INITIALIZATION_FAILURE', INVALID_SPLITS_RECEIVER = 'INVALID_SPLITS_RECEIVER', - CLIENT_INITIALIZATION_FAILURE = 'CLIENT_INITIALIZATION_FAILURE', INVALID_DRIPS_RECEIVER_CONFIG = 'INVALID_DRIPS_RECEIVER_CONFIG' } @@ -29,8 +29,7 @@ export class DripsError extends Error { } export class DripsErrors { - static clientInitializationError = (message: string) => - new DripsError(DripsErrorCode.CLIENT_INITIALIZATION_FAILURE, message); + static initializationError = (message: string) => new DripsError(DripsErrorCode.INITIALIZATION_FAILURE, message); static addressError = (message: string, address: string) => new DripsError(DripsErrorCode.INVALID_ADDRESS, message, { @@ -44,7 +43,7 @@ export class DripsErrors { }); static signerMissingError = ( - message: string = 'Tried to perform an operation that requires a signer, but a signer was not found. Did you create a read-only client instance?' + message: string = 'Tried to perform an operation that requires a signer but a signer was not found.' ) => new DripsError(DripsErrorCode.MISSING_SIGNER, message); static argumentMissingError = (message: string, argName: string) => diff --git a/src/common/validators.ts b/src/common/validators.ts index 875fad95..f60fd22e 100644 --- a/src/common/validators.ts +++ b/src/common/validators.ts @@ -151,25 +151,39 @@ export const validateSplitsReceivers = (receivers: SplitsReceiverStruct[]) => { /** @internal */ export const validateClientProvider = async (provider: Provider, supportedChains: readonly number[]) => { if (!provider) { - throw DripsErrors.argumentError(`'${nameOf({ provider })}' is missing.`); + throw DripsErrors.initializationError(`The provider is missing.`); } const network = await provider.getNetwork(); if (!supportedChains.includes(network?.chainId)) { - throw DripsErrors.unsupportedNetworkError( - `The provider is connected to an unsupported network with chain ID '${network?.chainId}' ('${network?.name}'). Supported chain IDs are: ${supportedChains}.`, - network?.chainId + throw DripsErrors.initializationError( + `The provider is connected to an unsupported network with chain ID '${network?.chainId}' ('${network?.name}'). Supported chain IDs are: ${supportedChains}.` ); } }; /** @internal */ -export const validateClientSigner = async (signer: Signer) => { +export const validateClientSigner = async (signer: Signer, supportedChains: readonly number[]) => { if (!signer) { - throw DripsErrors.argumentError(`'${nameOf({ signer })}' is missing.`); + throw DripsErrors.initializationError(`The singer is missing.`); } - validateAddress(await signer.getAddress()); + const address = await signer.getAddress(); + if (!ethers.utils.isAddress(address)) { + throw DripsErrors.initializationError(`Signer's address ('${address}') is not valid.`); + } + + const { provider } = signer; + if (!provider) { + throw DripsErrors.initializationError(`The signer has no provider.`); + } + + const network = await provider.getNetwork(); + if (!supportedChains.includes(network?.chainId)) { + throw DripsErrors.initializationError( + `The signer's provider is connected to an unsupported network with chain ID '${network?.chainId}' ('${network?.name}'). Supported chain IDs are: ${supportedChains}.` + ); + } }; /** @internal */ diff --git a/src/index.ts b/src/index.ts index 6cc2fb86..c8787459 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,7 +57,7 @@ export { default as DripsHubClient } from './DripsHub/DripsHubClient'; export { default as DripsSubgraphClient } from './DripsSubgraph/DripsSubgraphClient'; // ImmutableSplitsDriver -export { default as ImmutableSplitsDriverClient } from './ImmutableSplits/ImmutableSplitsDriver'; +export { default as ImmutableSplitsDriverClient } from './ImmutableSplits/ImmutableSplitsDriverClient'; // NFTDriver export { default as NFTDriverClient } from './NFTDriver/NFTDriverClient'; diff --git a/tests/Caller/CallerClient.tests.ts b/tests/Caller/CallerClient.tests.ts index 849f7445..776aa364 100644 --- a/tests/Caller/CallerClient.tests.ts +++ b/tests/Caller/CallerClient.tests.ts @@ -60,7 +60,7 @@ describe('CallerClient', () => { // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerStub), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -79,7 +79,7 @@ describe('CallerClient', () => { ); }); - it('should should throw a clientInitializationError when client cannot be initialized', async () => { + it('should should throw a initializationError when client cannot be initialized', async () => { // Arrange let threw = false; @@ -88,7 +88,7 @@ describe('CallerClient', () => { await CallerClient.create(undefined as any, undefined as any); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.CLIENT_INITIALIZATION_FAILURE); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } diff --git a/tests/DripsHub/DripsHubClient.tests.ts b/tests/DripsHub/DripsHubClient.tests.ts index 3d7627b6..8182e562 100644 --- a/tests/DripsHub/DripsHubClient.tests.ts +++ b/tests/DripsHub/DripsHubClient.tests.ts @@ -66,7 +66,7 @@ describe('DripsHubClient', () => { // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerStub), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -85,7 +85,7 @@ describe('DripsHubClient', () => { ); }); - it('should should throw a clientInitializationError when client cannot be initialized', async () => { + it('should should throw a initializationError when client cannot be initialized', async () => { // Arrange let threw = false; @@ -94,7 +94,7 @@ describe('DripsHubClient', () => { await DripsHubClient.create(undefined as any, undefined as any); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.CLIENT_INITIALIZATION_FAILURE); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } diff --git a/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.integration.tests.ts b/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.integration.tests.ts index 6f743ec5..e058ff63 100644 --- a/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.integration.tests.ts +++ b/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.integration.tests.ts @@ -2,7 +2,7 @@ import { InfuraProvider } from '@ethersproject/providers'; import { Wallet } from 'ethers'; import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import import { assert } from 'chai'; -import ImmutableSplitsDriver from '../../src/ImmutableSplits/ImmutableSplitsDriver'; +import ImmutableSplitsDriver from '../../src/ImmutableSplits/ImmutableSplitsDriverClient'; import AddressDriverClient from '../../src/AddressDriver/AddressDriverClient'; import DripsSubgraphClient from '../../src/DripsSubgraph/DripsSubgraphClient'; import type { SplitsReceiverStruct, UserMetadata } from '../../src/common/types'; diff --git a/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.tests.ts b/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.tests.ts index 896dfe6a..7590f128 100644 --- a/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.tests.ts +++ b/tests/ImmutableSplitsDriver/ImmutableSplitsDriverClient.tests.ts @@ -8,7 +8,7 @@ import sinon, { stubInterface, stubObject } from 'ts-sinon'; import { ImmutableSplitsDriver__factory } from '../../contracts/factories/ImmutableSplitsDriver__factory'; import type { ImmutableSplitsDriver, SplitsReceiverStruct } from '../../contracts/ImmutableSplitsDriver'; import DripsHubClient from '../../src/DripsHub/DripsHubClient'; -import ImmutableSplitsDriverClient from '../../src/ImmutableSplits/ImmutableSplitsDriver'; +import ImmutableSplitsDriverClient from '../../src/ImmutableSplits/ImmutableSplitsDriverClient'; import Utils from '../../src/utils'; import * as validators from '../../src/common/validators'; import type { UserMetadata } from '../../src/common/types'; @@ -67,7 +67,7 @@ describe('ImmutableSplitsDriverClient', () => { // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerStub), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -86,7 +86,7 @@ describe('ImmutableSplitsDriverClient', () => { ); }); - it('should should throw a clientInitializationError when client cannot be initialized', async () => { + it('should should throw a initializationError when client cannot be initialized', async () => { // Arrange let threw = false; @@ -95,7 +95,7 @@ describe('ImmutableSplitsDriverClient', () => { await ImmutableSplitsDriverClient.create(undefined as any, undefined as any); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.CLIENT_INITIALIZATION_FAILURE); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } diff --git a/tests/NFTDriver/NFTDriverClient.tests.ts b/tests/NFTDriver/NFTDriverClient.tests.ts index b851e8c4..272cea09 100644 --- a/tests/NFTDriver/NFTDriverClient.tests.ts +++ b/tests/NFTDriver/NFTDriverClient.tests.ts @@ -5,8 +5,8 @@ import sinon, { stubInterface, stubObject } from 'ts-sinon'; import type { ContractReceipt, ContractTransaction, Event } from 'ethers'; import { ethers, BigNumber, constants, Wallet } from 'ethers'; import { assert } from 'chai'; -import { IERC20, IERC20__factory, NFTDriver } from '../../contracts'; -import { NFTDriver__factory } from '../../contracts'; +import type { IERC20, NFTDriver } from '../../contracts'; +import { IERC20__factory, NFTDriver__factory } from '../../contracts'; import DripsHubClient from '../../src/DripsHub/DripsHubClient'; import NFTDriverClient from '../../src/NFTDriver/NFTDriverClient'; import Utils from '../../src/utils'; @@ -67,7 +67,7 @@ describe('NFTDriverClient', () => { // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerStub), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -86,7 +86,7 @@ describe('NFTDriverClient', () => { ); }); - it('should should throw a clientInitializationError when client cannot be initialized', async () => { + it('should should throw a initializationError when client cannot be initialized', async () => { // Arrange let threw = false; @@ -95,7 +95,7 @@ describe('NFTDriverClient', () => { await NFTDriverClient.create(undefined as any, undefined as any); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.CLIENT_INITIALIZATION_FAILURE); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } diff --git a/tests/common/DripsError.tests.ts b/tests/common/DripsError.tests.ts index 5a419739..c3de1558 100644 --- a/tests/common/DripsError.tests.ts +++ b/tests/common/DripsError.tests.ts @@ -16,17 +16,17 @@ describe('DripsErrors', () => { assert.equal(uniqueCodes.length, methods.length); }); - describe('clientInitializationError()', () => { + describe('initializationError()', () => { it('should return expected error details', () => { // Arrange const expectedMessage = 'Error'; // Act - const { code, message } = DripsErrors.clientInitializationError(expectedMessage); + const { code, message } = DripsErrors.initializationError(expectedMessage); // Assert assert.equal(message, expectedMessage); - assert.equal(code, DripsErrorCode.CLIENT_INITIALIZATION_FAILURE); + assert.equal(code, DripsErrorCode.INITIALIZATION_FAILURE); }); }); diff --git a/tests/common/validators.tests.ts b/tests/common/validators.tests.ts index 8946d922..50a03d5b 100644 --- a/tests/common/validators.tests.ts +++ b/tests/common/validators.tests.ts @@ -691,7 +691,7 @@ describe('validators', () => { await validators.validateClientProvider(undefined as unknown as JsonRpcProvider, []); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } @@ -714,7 +714,7 @@ describe('validators', () => { await validators.validateClientProvider(providerStub, [5]); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.UNSUPPORTED_NETWORK); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } @@ -730,10 +730,10 @@ describe('validators', () => { try { // Act - await validators.validateClientSigner(undefined as unknown as JsonRpcSigner); + await validators.validateClientSigner(undefined as unknown as JsonRpcSigner, Utils.Network.SUPPORTED_CHAINS); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } From e5f9ae9e8d7a63225f513ab0216d932e799b64c6 Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Fri, 10 Mar 2023 18:20:55 +0100 Subject: [PATCH 2/6] refactor(subgraph client): return current drips receivers in the order the protocol expects them --- src/DripsSubgraph/DripsSubgraphClient.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/DripsSubgraph/DripsSubgraphClient.ts b/src/DripsSubgraph/DripsSubgraphClient.ts index 1e8bab97..843504cb 100644 --- a/src/DripsSubgraph/DripsSubgraphClient.ts +++ b/src/DripsSubgraph/DripsSubgraphClient.ts @@ -3,7 +3,7 @@ import type { BigNumberish } from 'ethers'; import { ethers } from 'ethers'; import constants from '../constants'; -import { nameOf, valueFromString, keyFromString } from '../common/internals'; +import { nameOf, valueFromString, keyFromString, formatDripsReceivers } from '../common/internals'; import Utils from '../utils'; import { validateAddress } from '../common/validators'; import { DripsErrors } from '../common/DripsError'; @@ -878,7 +878,7 @@ export default class DripsSubgraphClient { // Filter by asset. const tokenDripsSetEvents = iterationDripsSetEvents.filter( - (e) => e.assetId == Utils.Asset.getIdFromAddress(tokenAddress) + (e) => e.assetId === Utils.Asset.getIdFromAddress(tokenAddress) ); dripsSetEvents.push(...tokenDripsSetEvents); @@ -897,11 +897,13 @@ export default class DripsSubgraphClient { // Sort by `blockTimestamp` DESC - the first ones will be the most recent. dripsSetEvents = dripsSetEvents.sort((a, b) => Number(b.blockTimestamp) - Number(a.blockTimestamp)); - // Return the most recent event's receivers. - return dripsSetEvents[0].dripsReceiverSeenEvents.map((d) => ({ - config: d.config, - userId: d.receiverUserId - })); + // Return the most recent event's receivers formatted as expected by the protocol. + return formatDripsReceivers( + dripsSetEvents[0].dripsReceiverSeenEvents.map((d) => ({ + config: d.config, + userId: d.receiverUserId + })) + ); } /** From 6ea16f961aff9c8d0a9ceee3d1ce70221dd0d5da Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Fri, 10 Mar 2023 18:21:35 +0100 Subject: [PATCH 3/6] refactor: reuse tx factory when setting drips in address drivers --- src/AddressDriver/AddressDriverClient.ts | 92 +++++++-------- src/AddressDriver/AddressDriverTxFactory.ts | 81 ++++++++++--- src/ERC20/ERC20TxFactory.ts | 3 +- .../AddressDriverClient.integration.tests.ts | 2 +- .../AddressDriverClient.tests.ts | 111 ++++-------------- .../AddressDriverTxFactory.tests.ts | 50 ++++++-- tests/ERC20/ERC20TxFactory.tests.ts | 30 +++-- 7 files changed, 194 insertions(+), 175 deletions(-) diff --git a/src/AddressDriver/AddressDriverClient.ts b/src/AddressDriver/AddressDriverClient.ts index 925bfe93..eab77b7f 100644 --- a/src/AddressDriver/AddressDriverClient.ts +++ b/src/AddressDriver/AddressDriverClient.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-dupe-class-members */ import type { Provider } from '@ethersproject/providers'; import type { BigNumberish, ContractTransaction, Signer } from 'ethers'; import { ethers, BigNumber, constants } from 'ethers'; @@ -18,11 +19,12 @@ import { IERC20__factory, AddressDriver__factory } from '../../contracts'; import { nameOf, isNullOrUndefined, - formatDripsReceivers, formatSplitReceivers, ensureSignerExists, createFromStrings } from '../common/internals'; +import type { IAddressDriverTxFactory } from './AddressDriverTxFactory'; +import AddressDriverTxFactory from './AddressDriverTxFactory'; /** * A client for managing Drips accounts identified by Ethereum addresses. @@ -33,6 +35,7 @@ export default class AddressDriverClient { #driverAddress!: string; #provider!: Provider; #signer: Signer | undefined; + #addressDriverTxFactory!: IAddressDriverTxFactory; /** Returns the client's `provider`. */ public get provider(): Provider { @@ -66,41 +69,55 @@ export default class AddressDriverClient { * @param {Signer} signer The singer used to sign transactions. It cannot be changed after creation. * * **Important**: If the `signer` is _not_ connected to a provider it will try to connect to the `provider`, else it will use the `signer.provider`. - * @param {string|undefined} customDriverAddress Overrides the `NFTDriver` contract address. + * @param {string|undefined} customDriverAddress Overrides the `AddressDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. - * @throws {@link DripsErrors.clientInitializationError} if the client initialization fails. + * @throws {@link DripsErrors.initializationError} if the client initialization fails. */ public static async create( provider: Provider, signer?: Signer, customDriverAddress?: string + ): Promise; + public static async create( + provider: Provider, + signer?: Signer, + customDriverAddress?: string, + addressDriverTxFactory?: IAddressDriverTxFactory + ): Promise; + public static async create( + provider: Provider, + signer?: Signer, + customDriverAddress?: string, + addressDriverTxFactory?: IAddressDriverTxFactory ): Promise { - try { - await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); - - if (signer) { - await validateClientSigner(signer); + await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); - if (!signer.provider) { - // eslint-disable-next-line no-param-reassign - signer = signer.connect(provider); - } + if (signer) { + if (!signer.provider) { + // eslint-disable-next-line no-param-reassign + signer = signer.connect(provider); } - const network = await provider.getNetwork(); - const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_ADDRESS_DRIVER; + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); + } + + const network = await provider.getNetwork(); + const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_ADDRESS_DRIVER; - const client = new AddressDriverClient(); + const client = new AddressDriverClient(); - client.#signer = signer; - client.#provider = provider; - client.#driverAddress = driverAddress; - client.#driver = AddressDriver__factory.connect(driverAddress, signer ?? provider); - return client; - } catch (error: any) { - throw DripsErrors.clientInitializationError(`Could not create 'AddressDriverClient': ${error.message}`); + client.#signer = signer; + client.#provider = provider; + client.#driverAddress = driverAddress; + client.#driver = AddressDriver__factory.connect(driverAddress, signer ?? provider); + + if (signer) { + client.#addressDriverTxFactory = + addressDriverTxFactory || (await AddressDriverTxFactory.create(signer, customDriverAddress)); } + + return client; } /** @@ -302,6 +319,7 @@ export default class AddressDriverClient { balanceDelta: BigNumberish = 0 ): Promise { ensureSignerExists(this.#signer); + validateSetDripsInput( tokenAddress, currentReceivers?.map((r) => ({ @@ -316,35 +334,17 @@ export default class AddressDriverClient { balanceDelta ); - const formattedCurrentReceivers = formatDripsReceivers(currentReceivers); - const formattedNewReceivers = formatDripsReceivers(newReceivers); - - const estimatedGasFees = ( - await this.#driver.estimateGas.setDrips( - tokenAddress, - formattedCurrentReceivers, - balanceDelta, - formattedNewReceivers, - 0, - 0, - transferToAddress - ) - ).toNumber(); - - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - return this.#driver.setDrips( + const tx = await this.#addressDriverTxFactory.setDrips( tokenAddress, - formattedCurrentReceivers, + currentReceivers, balanceDelta, - formattedNewReceivers, + newReceivers, 0, 0, - transferToAddress, - { - gasLimit - } + transferToAddress ); + + return this.#signer.sendTransaction(tx); } /** diff --git a/src/AddressDriver/AddressDriverTxFactory.ts b/src/AddressDriver/AddressDriverTxFactory.ts index b341dca3..802a256a 100644 --- a/src/AddressDriver/AddressDriverTxFactory.ts +++ b/src/AddressDriver/AddressDriverTxFactory.ts @@ -1,5 +1,4 @@ /* eslint-disable no-dupe-class-members */ -import type { Provider } from '@ethersproject/providers'; import type { AddressDriver, DripsReceiverStruct, @@ -7,18 +6,23 @@ import type { UserMetadataStruct } from 'contracts/AddressDriver'; import type { PromiseOrValue } from 'contracts/common'; -import type { PopulatedTransaction, BigNumberish } from 'ethers'; +import type { PopulatedTransaction, BigNumberish, Overrides, Signer } from 'ethers'; +import { formatDripsReceivers } from '../common/internals'; import { AddressDriver__factory } from '../../contracts/factories'; -import { validateClientProvider } from '../common/validators'; +import { validateClientSigner } from '../common/validators'; import Utils from '../utils'; -interface IAddressDriverTxFactory +export interface IAddressDriverTxFactory extends Pick< AddressDriver['populateTransaction'], 'collect' | 'give' | 'setSplits' | 'setDrips' | 'emitUserMetadata' > {} +/** + * A factory for creating `AddressDriver` contract transactions. + */ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { + #signer!: Signer; #driver!: AddressDriver; #driverAddress!: string; @@ -26,17 +30,37 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { return this.#driverAddress; } - public static async create(provider: Provider, customDriverAddress?: string): Promise { - await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); + public get signer(): Signer | undefined { + return this.#signer; + } - const network = await provider.getNetwork(); - const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_ADDRESS_DRIVER; + // TODO: Update the supported chains documentation comments. + /** + * Creates a new immutable `AddressDriverTxFactory` instance. + * + * @param signer The signer that will be used to sign the generated transactions. + * + * The `singer` must be connected to a provider. + * + * The supported networks are: + * - 'goerli': chain ID `5` + * - 'polygon-mumbai': chain ID `80001` + * @param customDriverAddress Overrides the `AddressDriver` contract address. + * If it's `undefined` (default value), the address will be automatically selected based on the `signer.provider`'s network. + * @returns A `Promise` which resolves to the new client instance. + * @throws {@link DripsErrors.initializationError} if the initialization fails. + */ + public static async create(signer: Signer, customDriverAddress?: string): Promise { + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); - const client = new AddressDriverTxFactory(); + const { chainId } = await signer.provider!.getNetwork(); // If the validation passed we know that the signer is connected to a provider. - client.#driverAddress = driverAddress; + const driverAddress = customDriverAddress || Utils.Network.configs[chainId].CONTRACT_ADDRESS_DRIVER; - client.#driver = AddressDriver__factory.connect(driverAddress, provider); + const client = new AddressDriverTxFactory(); + client.#signer = signer; + client.#driverAddress = driverAddress; + client.#driver = AddressDriver__factory.connect(driverAddress, signer); return client; } @@ -57,21 +81,42 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { return this.#driver.populateTransaction.setSplits(receivers); } - setDrips( + public async setDrips( erc20: PromiseOrValue, currReceivers: DripsReceiverStruct[], balanceDelta: PromiseOrValue, newReceivers: DripsReceiverStruct[], - transferTo: PromiseOrValue + maxEndHint1: PromiseOrValue, + maxEndHint2: PromiseOrValue, + transferTo: PromiseOrValue, + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { + if (!overrides.gasLimit) { + const gasEstimation = await this.#driver.estimateGas.setDrips( + erc20, + formatDripsReceivers(currReceivers), + balanceDelta, + formatDripsReceivers(newReceivers), + maxEndHint1, + maxEndHint2, + transferTo, + overrides + ); + + const gasLimit = Math.ceil(gasEstimation.toNumber() * 1.2); + // eslint-disable-next-line no-param-reassign + overrides = { ...overrides, gasLimit }; + } + return this.#driver.populateTransaction.setDrips( erc20, - currReceivers, + formatDripsReceivers(currReceivers), balanceDelta, - newReceivers, - 0, - 0, - transferTo + formatDripsReceivers(newReceivers), + maxEndHint1, + maxEndHint2, + transferTo, + overrides ); } diff --git a/src/ERC20/ERC20TxFactory.ts b/src/ERC20/ERC20TxFactory.ts index 2d56ea69..c746cafe 100644 --- a/src/ERC20/ERC20TxFactory.ts +++ b/src/ERC20/ERC20TxFactory.ts @@ -1,6 +1,7 @@ /* eslint-disable no-dupe-class-members */ import type { PromiseOrValue } from 'contracts/common'; import type { PopulatedTransaction, BigNumberish, Signer } from 'ethers'; +import { Utils } from 'radicle-drips'; import type { IERC20 } from '../../contracts'; import { IERC20__factory } from '../../contracts/factories'; import { validateClientSigner } from '../common/validators'; @@ -16,7 +17,7 @@ export default class ERC20TxFactory implements IERC20TxFactory { } public static async create(singer: Signer, tokenAddress: string): Promise { - await validateClientSigner(singer); + await validateClientSigner(singer, Utils.Network.SUPPORTED_CHAINS); const client = new ERC20TxFactory(); diff --git a/tests/AddressDriver/AddressDriverClient.integration.tests.ts b/tests/AddressDriver/AddressDriverClient.integration.tests.ts index 2e02ef71..f649b578 100644 --- a/tests/AddressDriver/AddressDriverClient.integration.tests.ts +++ b/tests/AddressDriver/AddressDriverClient.integration.tests.ts @@ -33,7 +33,7 @@ describe('AddressDriver integration tests', () => { account2AddressDriverClient = await AddressDriverClient.create(provider, account2AsSigner); }); - it('should set Drips configuration', async () => { + it.only('should set Drips configuration', async () => { console.log(`Will update WETH (${WETH}) Drips configuration for ${account2}.`); const userId1 = await account1AddressDriverClient.getUserId(); diff --git a/tests/AddressDriver/AddressDriverClient.tests.ts b/tests/AddressDriver/AddressDriverClient.tests.ts index 278b6852..cdbf171c 100644 --- a/tests/AddressDriver/AddressDriverClient.tests.ts +++ b/tests/AddressDriver/AddressDriverClient.tests.ts @@ -4,6 +4,7 @@ import { assert } from 'chai'; import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import type { Network } from '@ethersproject/networks'; import { BigNumber, constants, ethers, Wallet } from 'ethers'; +import { AddressDriverTxFactory } from 'radicle-drips'; import type { AddressDriver, IERC20 } from '../../contracts'; import { IERC20__factory, AddressDriver__factory } from '../../contracts'; import type { SplitsReceiverStruct, DripsReceiverStruct, UserMetadata } from '../../src/common/types'; @@ -22,6 +23,7 @@ describe('AddressDriverClient', () => { let dripsHubClientStub: StubbedInstance; let providerStub: sinon.SinonStubbedInstance; let addressDriverContractStub: StubbedInstance; + let addressDriverTxFactoryStub: StubbedInstance; let signerWithProviderStub: StubbedInstance; let testAddressDriverClient: AddressDriverClient; @@ -49,7 +51,18 @@ describe('AddressDriverClient', () => { dripsHubClientStub = stubInterface(); sinon.stub(DripsHubClient, 'create').resolves(dripsHubClientStub); - testAddressDriverClient = await AddressDriverClient.create(providerStub, signerStub); + addressDriverTxFactoryStub = stubInterface(); + sinon + .stub(AddressDriverTxFactory, 'create') + .withArgs(providerStub, Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER) + .resolves(addressDriverTxFactoryStub); + + testAddressDriverClient = await AddressDriverClient.create( + providerStub, + signerStub, + undefined, + addressDriverTxFactoryStub + ); }); afterEach(() => { @@ -66,7 +79,7 @@ describe('AddressDriverClient', () => { // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerStub), + validateClientSignerStub.calledWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -80,12 +93,12 @@ describe('AddressDriverClient', () => { // Assert assert( - validateClientProviderStub.calledOnceWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), + validateClientProviderStub.calledWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); - it('should should throw a clientInitializationError when client cannot be initialized', async () => { + it('should should throw a initializationError when client cannot be initialized', async () => { // Arrange let threw = false; @@ -94,7 +107,7 @@ describe('AddressDriverClient', () => { await AddressDriverClient.create(undefined as any, undefined as any); } catch (error: any) { // Assert - assert.equal(error.code, DripsErrorCode.CLIENT_INITIALIZATION_FAILURE); + assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); threw = true; } @@ -585,44 +598,7 @@ describe('AddressDriverClient', () => { ); }); - it('should clear drips when new receivers is an empty list', async () => { - // Arrange - const tokenAddress = Wallet.createRandom().address; - const transferToAddress = Wallet.createRandom().address; - const currentReceivers: DripsReceiverStruct[] = [ - { - userId: 3, - config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 3n, duration: 3n, start: 3n }) - } - ]; - - const estimatedGasFees = 2000000; - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - addressDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - - // Act - await testAddressDriverClient.setDrips(tokenAddress, currentReceivers, [], transferToAddress, 1n); - - // Assert - assert( - addressDriverContractStub.setDrips.calledOnceWithExactly( - tokenAddress, - currentReceivers, - 1n, - [], - 0, - 0, - transferToAddress, - { gasLimit } - ), - 'Expected method to be called with different arguments' - ); - }); - - it('should call the setDrips() method of the AddressDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const tokenAddress = Wallet.createRandom().address; const transferToAddress = Wallet.createRandom().address; @@ -647,57 +623,14 @@ describe('AddressDriverClient', () => { } ]; - const estimatedGasFees = 2000000; - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - addressDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; + const tx = {}; + addressDriverTxFactoryStub.setDrips.resolves(tx); // Act await testAddressDriverClient.setDrips(tokenAddress, currentReceivers, receivers, transferToAddress, 1n); // Assert - assert( - addressDriverContractStub.setDrips.calledOnceWithExactly( - tokenAddress, - currentReceivers, - 1n, - sinon - .match((r: DripsReceiverStruct[]) => r[0].userId === 1n) - .and(sinon.match((r: DripsReceiverStruct[]) => r[1].userId === 2n)) - .and(sinon.match((r: DripsReceiverStruct[]) => r.length === 2)), - 0, - 0, - transferToAddress, - { gasLimit } - ), - 'Expected method to be called with different arguments' - ); - }); - - it('should set balanceDelta to 0 when balanceDelta is undefined', async () => { - // Arrange - const tokenAddress = Wallet.createRandom().address; - const transferToAddress = Wallet.createRandom().address; - - const estimatedGasFees = 2000000; - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - addressDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - - // Act - await testAddressDriverClient.setDrips(tokenAddress, [], [], transferToAddress, undefined as unknown as bigint); - - // Assert - assert( - addressDriverContractStub.setDrips.calledOnceWithExactly(tokenAddress, [], 0, [], 0, 0, transferToAddress, { - gasLimit - }), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Expected method to be called'); }); }); diff --git a/tests/AddressDriver/AddressDriverTxFactory.tests.ts b/tests/AddressDriver/AddressDriverTxFactory.tests.ts index 218b04d8..6ffce91c 100644 --- a/tests/AddressDriver/AddressDriverTxFactory.tests.ts +++ b/tests/AddressDriver/AddressDriverTxFactory.tests.ts @@ -1,20 +1,23 @@ import type { Network } from '@ethersproject/networks'; -import { JsonRpcProvider } from '@ethersproject/providers'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { assert } from 'chai'; import type { StubbedInstance } from 'ts-sinon'; import sinon, { stubObject, stubInterface } from 'ts-sinon'; -import { Wallet } from 'ethers'; +import { BigNumber, Wallet } from 'ethers'; import type { AddressDriver } from '../../contracts'; import { AddressDriver__factory } from '../../contracts'; import Utils from '../../src/utils'; import AddressDriverTxFactory from '../../src/AddressDriver/AddressDriverTxFactory'; import * as validators from '../../src/common/validators'; import type { SplitsReceiverStruct, DripsReceiverStruct, UserMetadataStruct } from '../../src/common/types'; +import { formatDripsReceivers } from '../../src/common/internals'; describe('AddressDriverTxFactory', () => { const TEST_CHAIN_ID = 5; // Goerli. let networkStub: StubbedInstance; + let signerStub: StubbedInstance; + let signerWithProviderStub: StubbedInstance; let providerStub: sinon.SinonStubbedInstance; let addressDriverContractStub: StubbedInstance; @@ -23,16 +26,24 @@ describe('AddressDriverTxFactory', () => { // Acts also as the "base Arrange step". beforeEach(async () => { providerStub = sinon.createStubInstance(JsonRpcProvider); + + signerStub = sinon.createStubInstance(JsonRpcSigner); + signerStub.getAddress.resolves(Wallet.createRandom().address); + networkStub = stubObject({ chainId: TEST_CHAIN_ID } as Network); + providerStub.getNetwork.resolves(networkStub); + signerWithProviderStub = { ...signerStub, provider: providerStub }; + signerStub.connect.withArgs(providerStub).returns(signerWithProviderStub); + addressDriverContractStub = stubInterface(); sinon .stub(AddressDriver__factory, 'connect') - .withArgs(Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER, providerStub) + .withArgs(Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER, signerWithProviderStub) .returns(addressDriverContractStub); - testAddressDriverTxFactory = await AddressDriverTxFactory.create(providerStub); + testAddressDriverTxFactory = await AddressDriverTxFactory.create(signerWithProviderStub); }); afterEach(() => { @@ -40,16 +51,16 @@ describe('AddressDriverTxFactory', () => { }); describe('create', async () => { - it('should validate the provider', async () => { + it('should validate the signer', async () => { // Arrange - const validateClientProviderStub = sinon.stub(validators, 'validateClientProvider'); + const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); // Act - await AddressDriverTxFactory.create(providerStub); + await AddressDriverTxFactory.create(signerWithProviderStub); // Assert assert( - validateClientProviderStub.calledOnceWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -59,7 +70,7 @@ describe('AddressDriverTxFactory', () => { const customDriverAddress = Wallet.createRandom().address; // Act - const client = await AddressDriverTxFactory.create(providerStub, customDriverAddress); + const client = await AddressDriverTxFactory.create(signerWithProviderStub, customDriverAddress); // Assert assert.equal(client.driverAddress, customDriverAddress); @@ -122,14 +133,27 @@ describe('AddressDriverTxFactory', () => { // Arrange const stub = sinon.stub(); addressDriverContractStub.populateTransaction.setDrips = stub; - const currReceivers = [] as DripsReceiverStruct[]; - const newReceivers = [] as DripsReceiverStruct[]; + const currReceivers = [{ userId: 2 }, { userId: 1 }] as DripsReceiverStruct[]; + const newReceivers = [{ userId: 2 }, { userId: 1 }] as DripsReceiverStruct[]; + + addressDriverContractStub.estimateGas.setDrips = sinon.stub().resolves(BigNumber.from(100)); // Act - await testAddressDriverTxFactory.setDrips('0x1234', currReceivers, '0x5678', newReceivers, '0x9abc'); + await testAddressDriverTxFactory.setDrips('0x1234', currReceivers, '0x5678', newReceivers, 0, 0, '0x9abc'); // Assert - assert(stub.calledOnceWithExactly('0x1234', currReceivers, '0x5678', newReceivers, 0, 0, '0x9abc')); + assert( + stub.calledOnceWithExactly( + '0x1234', + formatDripsReceivers(currReceivers), + '0x5678', + formatDripsReceivers(newReceivers), + 0, + 0, + '0x9abc', + { gasLimit: 120 } + ) + ); }); }); diff --git a/tests/ERC20/ERC20TxFactory.tests.ts b/tests/ERC20/ERC20TxFactory.tests.ts index 42ba0b6b..3b0818d5 100644 --- a/tests/ERC20/ERC20TxFactory.tests.ts +++ b/tests/ERC20/ERC20TxFactory.tests.ts @@ -1,30 +1,46 @@ -import { JsonRpcSigner } from '@ethersproject/providers'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { assert } from 'chai'; import type { StubbedInstance } from 'ts-sinon'; -import sinon, { stubInterface } from 'ts-sinon'; +import sinon, { stubObject, stubInterface } from 'ts-sinon'; import { Wallet } from 'ethers'; +import type { Network } from '@ethersproject/networks'; import type { IERC20 } from '../../contracts'; import { IERC20__factory } from '../../contracts'; import ERC20TxFactory from '../../src/ERC20/ERC20TxFactory'; import * as validators from '../../src/common/validators'; +import Utils from '../../src/utils'; describe('ERC20TxFactory', () => { const TOKEN_ADDRESS = Wallet.createRandom().address; - let signerStub: sinon.SinonStubbedInstance; + let networkStub: StubbedInstance; let IERC20ContractStub: StubbedInstance; + let signerStub: sinon.SinonStubbedInstance; + let signerWithProviderStub: StubbedInstance; + let providerStub: sinon.SinonStubbedInstance; let testERC20TxFactory: ERC20TxFactory; // Acts also as the "base Arrange step". beforeEach(async () => { + const TEST_CHAIN_ID = 5; // Goerli. + + providerStub = sinon.createStubInstance(JsonRpcProvider); + signerStub = sinon.createStubInstance(JsonRpcSigner); signerStub.getAddress.resolves(Wallet.createRandom().address); + networkStub = stubObject({ chainId: TEST_CHAIN_ID } as Network); + + providerStub.getNetwork.resolves(networkStub); + + signerWithProviderStub = { ...signerStub, provider: providerStub }; + signerStub.connect.withArgs(providerStub).returns(signerWithProviderStub); + IERC20ContractStub = stubInterface(); - sinon.stub(IERC20__factory, 'connect').withArgs(TOKEN_ADDRESS, signerStub).returns(IERC20ContractStub); + sinon.stub(IERC20__factory, 'connect').withArgs(TOKEN_ADDRESS, signerWithProviderStub).returns(IERC20ContractStub); - testERC20TxFactory = await ERC20TxFactory.create(signerStub, TOKEN_ADDRESS); + testERC20TxFactory = await ERC20TxFactory.create(signerWithProviderStub, TOKEN_ADDRESS); }); afterEach(() => { @@ -37,11 +53,11 @@ describe('ERC20TxFactory', () => { const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); // Act - await ERC20TxFactory.create(signerStub, TOKEN_ADDRESS); + await ERC20TxFactory.create(signerWithProviderStub, TOKEN_ADDRESS); // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerStub), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); From c35c17684300d444e522c575857cdd01f85d5dec Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Mon, 13 Mar 2023 10:49:48 +0100 Subject: [PATCH 4/6] refactor(address driver client): reuse tx factories instead of the generated API --- src/AddressDriver/AddressDriverClient.ts | 80 ++++++++--------- src/AddressDriver/AddressDriverTxFactory.ts | 15 ++-- src/ERC20/ERC20TxFactory.ts | 5 +- .../AddressDriverClient.integration.tests.ts | 2 +- .../AddressDriverClient.tests.ts | 89 ++++++++----------- .../AddressDriverTxFactory.tests.ts | 1 + 6 files changed, 90 insertions(+), 102 deletions(-) diff --git a/src/AddressDriver/AddressDriverClient.ts b/src/AddressDriver/AddressDriverClient.ts index eab77b7f..9d505820 100644 --- a/src/AddressDriver/AddressDriverClient.ts +++ b/src/AddressDriver/AddressDriverClient.ts @@ -16,13 +16,7 @@ import Utils from '../utils'; import { DripsErrors } from '../common/DripsError'; import type { AddressDriver } from '../../contracts'; import { IERC20__factory, AddressDriver__factory } from '../../contracts'; -import { - nameOf, - isNullOrUndefined, - formatSplitReceivers, - ensureSignerExists, - createFromStrings -} from '../common/internals'; +import { nameOf, isNullOrUndefined, ensureSignerExists, createFromStrings } from '../common/internals'; import type { IAddressDriverTxFactory } from './AddressDriverTxFactory'; import AddressDriverTxFactory from './AddressDriverTxFactory'; @@ -31,11 +25,11 @@ import AddressDriverTxFactory from './AddressDriverTxFactory'; * @see {@link https://github.com/radicle-dev/drips-contracts/blob/master/src/AddressDriver.sol AddressDriver} contract. */ export default class AddressDriverClient { + #provider!: Provider; #driver!: AddressDriver; #driverAddress!: string; - #provider!: Provider; #signer: Signer | undefined; - #addressDriverTxFactory!: IAddressDriverTxFactory; + #txFactory!: IAddressDriverTxFactory; /** Returns the client's `provider`. */ public get provider(): Provider { @@ -61,15 +55,15 @@ export default class AddressDriverClient { // TODO: Update the supported chains documentation comments. /** * Creates a new immutable `AddressDriverClient` instance. - * @param {Provider} provider The network provider. It cannot be changed after creation. + * @param provider The network provider. It cannot be changed after creation. * * The `provider` must be connected to one of the following supported networks: * - 'goerli': chain ID `5` * - 'polygon-mumbai': chain ID `80001` - * @param {Signer} signer The singer used to sign transactions. It cannot be changed after creation. + * @param signer The singer used to sign transactions. It cannot be changed after creation. * * **Important**: If the `signer` is _not_ connected to a provider it will try to connect to the `provider`, else it will use the `signer.provider`. - * @param {string|undefined} customDriverAddress Overrides the `AddressDriver` contract address. + * @param customDriverAddress Overrides the `AddressDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. * @throws {@link DripsErrors.initializationError} if the client initialization fails. @@ -83,13 +77,13 @@ export default class AddressDriverClient { provider: Provider, signer?: Signer, customDriverAddress?: string, - addressDriverTxFactory?: IAddressDriverTxFactory + txFactory?: IAddressDriverTxFactory ): Promise; public static async create( provider: Provider, signer?: Signer, customDriverAddress?: string, - addressDriverTxFactory?: IAddressDriverTxFactory + txFactory?: IAddressDriverTxFactory ): Promise { await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); @@ -113,8 +107,7 @@ export default class AddressDriverClient { client.#driver = AddressDriver__factory.connect(driverAddress, signer ?? provider); if (signer) { - client.#addressDriverTxFactory = - addressDriverTxFactory || (await AddressDriverTxFactory.create(signer, customDriverAddress)); + client.#txFactory = txFactory || (await AddressDriverTxFactory.create(signer, customDriverAddress)); } return client; @@ -122,7 +115,7 @@ export default class AddressDriverClient { /** * Returns the remaining number of tokens the `AddressDriver` contract is allowed to spend on behalf of the user for the given ERC20 token. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. @@ -148,7 +141,7 @@ export default class AddressDriverClient { /** * Sets the maximum allowance value for the `AddressDriver` contract over the user's tokens for the given ERC20 token. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. @@ -187,7 +180,7 @@ export default class AddressDriverClient { /** * Returns the user ID for a given address. - * @param {string} userAddress The user address. + * @param userAddress The user address. * @returns A `Promise` which resolves to the user ID. * @throws {@link DripsErrors.addressError} if the `userAddress` address is not valid. */ @@ -201,14 +194,14 @@ export default class AddressDriverClient { /** * Collects the received and already split funds and transfers them from the `DripsHub` contract to an address. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. * Tokens which rebase the holders' balances, collect taxes on transfers, * or impose any restrictions on holding or transferring tokens are not supported. * If you use such tokens in the protocol, they can get stuck or lost. - * @param {string} transferToAddress The address to send collected funds to. + * @param transferToAddress The address to send collected funds to. * @returns A `Promise` which resolves to the contract transaction. * @throws {@link DripsErrors.addressError} if `tokenAddress` or `transferToAddress` is not valid. * @throws {@link DripsErrors.signerMissingError} if the provider's signer is missing. @@ -217,29 +210,31 @@ export default class AddressDriverClient { ensureSignerExists(this.#signer); validateCollectInput(tokenAddress, transferToAddress); - return this.#driver.collect(tokenAddress, transferToAddress); + const tx = await this.#txFactory.collect(tokenAddress, transferToAddress); + + return this.#signer.sendTransaction(tx); } /** * Gives funds to the receiver. * The receiver can collect them immediately. * Transfers funds from the user's wallet to the `DripsHub` contract. - * @param {string} receiverUserId The receiver user ID. - * @param {string} tokenAddress The ERC20 token address. + * @param receiverUserId The receiver user ID. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. * Tokens which rebase the holders' balances, collect taxes on transfers, * or impose any restrictions on holding or transferring tokens are not supported. * If you use such tokens in the protocol, they can get stuck or lost. - * @param {BigNumberish} amount The amount to give (in the smallest unit, e.g., Wei). It must be greater than `0`. + * @param amount The amount to give (in the smallest unit, e.g., Wei). It must be greater than `0`. * @returns A `Promise` which resolves to the contract transaction. * @throws {@link DripsErrors.argumentMissingError} if the `receiverUserId` is missing. * @throws {@link DripsErrors.addressError} if the `tokenAddress` is not valid. * @throws {@link DripsErrors.argumentError} if the `amount` is less than or equal to `0`. * @throws {@link DripsErrors.signerMissingError} if the provider's signer is missing. */ - public give(receiverUserId: string, tokenAddress: string, amount: BigNumberish): Promise { + public async give(receiverUserId: string, tokenAddress: string, amount: BigNumberish): Promise { ensureSignerExists(this.#signer); if (isNullOrUndefined(receiverUserId)) { @@ -259,12 +254,14 @@ export default class AddressDriverClient { ); } - return this.#driver.give(receiverUserId, tokenAddress, amount); + const tx = await this.#txFactory.give(receiverUserId, tokenAddress, amount); + + return this.#signer.sendTransaction(tx); } /** * Sets the Splits configuration. - * @param {SplitsReceiverStruct[]} receivers The splits receivers (max `200`). + * @param receivers The splits receivers (max `200`). * Each splits receiver will be getting `weight / TOTAL_SPLITS_WEIGHT` share of the funds. * Duplicate receivers are not allowed and will only be processed once. * Pass an empty array if you want to clear all receivers. @@ -274,11 +271,13 @@ export default class AddressDriverClient { * @throws {@link DripsErrors.splitsReceiverError} if any of the `receivers` is not valid. * @throws {@link DripsErrors.signerMissingError} if the provider's signer is missing. */ - public setSplits(receivers: SplitsReceiverStruct[]): Promise { + public async setSplits(receivers: SplitsReceiverStruct[]): Promise { ensureSignerExists(this.#signer); validateSplitsReceivers(receivers); - return this.#driver.setSplits(formatSplitReceivers(receivers)); + const tx = await this.#txFactory.setSplits(receivers); + + return this.#signer.sendTransaction(tx); } /** @@ -291,15 +290,15 @@ export default class AddressDriverClient { * Tokens which rebase the holders' balances, collect taxes on transfers, * or impose any restrictions on holding or transferring tokens are not supported. * If you use such tokens in the protocol, they can get stuck or lost. - * @param {DripsReceiverStruct[]} currentReceivers The drips receivers that were set in the last drips update. + * @param currentReceivers The drips receivers that were set in the last drips update. * Pass an empty array if this is the first update. * * **Tip**: you might want to use `DripsSubgraphClient.getCurrentDripsReceivers` to easily retrieve the list of current receivers. - * @param {DripsReceiverStruct[]} newReceivers The new drips receivers (max `100`). + * @param newReceivers The new drips receivers (max `100`). * Duplicate receivers are not allowed and will only be processed once. * Pass an empty array if you want to clear all receivers. - * @param {string} transferToAddress The address to send funds to in case of decreasing balance. - * @param {BigNumberish} balanceDelta The drips balance change to be applied: + * @param transferToAddress The address to send funds to in case of decreasing balance. + * @param balanceDelta The drips balance change to be applied: * - Positive to add funds to the drips balance. * - Negative to remove funds from the drips balance. * - `0` to leave drips balance as is (default value). @@ -319,7 +318,6 @@ export default class AddressDriverClient { balanceDelta: BigNumberish = 0 ): Promise { ensureSignerExists(this.#signer); - validateSetDripsInput( tokenAddress, currentReceivers?.map((r) => ({ @@ -334,7 +332,7 @@ export default class AddressDriverClient { balanceDelta ); - const tx = await this.#addressDriverTxFactory.setDrips( + const tx = await this.#txFactory.setDrips( tokenAddress, currentReceivers, balanceDelta, @@ -350,25 +348,27 @@ export default class AddressDriverClient { /** * Emits the user's metadata. * The key and the value are _not_ standardized by the protocol, it's up to the user to establish and follow conventions to ensure compatibility with the consumers. - * @param {UserMetadata[]} userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. + * @param userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. * * **Tip**: you might want to use `Utils.UserMetadata.createFromStrings` to easily create metadata instances from `string` inputs. * @returns A `Promise` which resolves to the contract transaction. * @throws {@link DripsErrors.argumentError} if any of the metadata entries is not valid. * @throws {@link DripsErrors.signerMissingError} if the provider's signer is missing. */ - public emitUserMetadata(userMetadata: UserMetadata[]): Promise { + public async emitUserMetadata(userMetadata: UserMetadata[]): Promise { ensureSignerExists(this.#signer); validateEmitUserMetadataInput(userMetadata); const userMetadataAsBytes = userMetadata.map((m) => createFromStrings(m.key, m.value)); - return this.#driver.emitUserMetadata(userMetadataAsBytes); + const tx = await this.#txFactory.emitUserMetadata(userMetadataAsBytes); + + return this.#signer.sendTransaction(tx); } /** * Returns a user's address given a user ID. - * @param {string} userId The user ID. + * @param userId The user ID. * @returns The user's address. */ public static getUserAddress = (userId: string): string => { diff --git a/src/AddressDriver/AddressDriverTxFactory.ts b/src/AddressDriver/AddressDriverTxFactory.ts index 802a256a..f090eac9 100644 --- a/src/AddressDriver/AddressDriverTxFactory.ts +++ b/src/AddressDriver/AddressDriverTxFactory.ts @@ -7,7 +7,7 @@ import type { } from 'contracts/AddressDriver'; import type { PromiseOrValue } from 'contracts/common'; import type { PopulatedTransaction, BigNumberish, Overrides, Signer } from 'ethers'; -import { formatDripsReceivers } from '../common/internals'; +import { formatDripsReceivers, formatSplitReceivers } from '../common/internals'; import { AddressDriver__factory } from '../../contracts/factories'; import { validateClientSigner } from '../common/validators'; import Utils from '../utils'; @@ -65,11 +65,14 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { return client; } - collect(erc20: PromiseOrValue, transferTo: PromiseOrValue): Promise { + public async collect( + erc20: PromiseOrValue, + transferTo: PromiseOrValue + ): Promise { return this.#driver.populateTransaction.collect(erc20, transferTo); } - give( + public async give( receiver: PromiseOrValue, erc20: PromiseOrValue, amt: PromiseOrValue @@ -77,8 +80,8 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { return this.#driver.populateTransaction.give(receiver, erc20, amt); } - setSplits(receivers: SplitsReceiverStruct[]): Promise { - return this.#driver.populateTransaction.setSplits(receivers); + public async setSplits(receivers: SplitsReceiverStruct[]): Promise { + return this.#driver.populateTransaction.setSplits(formatSplitReceivers(receivers)); } public async setDrips( @@ -120,7 +123,7 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { ); } - emitUserMetadata(userMetadata: UserMetadataStruct[]): Promise { + public async emitUserMetadata(userMetadata: UserMetadataStruct[]): Promise { return this.#driver.populateTransaction.emitUserMetadata(userMetadata); } } diff --git a/src/ERC20/ERC20TxFactory.ts b/src/ERC20/ERC20TxFactory.ts index c746cafe..140b5e24 100644 --- a/src/ERC20/ERC20TxFactory.ts +++ b/src/ERC20/ERC20TxFactory.ts @@ -1,13 +1,16 @@ /* eslint-disable no-dupe-class-members */ import type { PromiseOrValue } from 'contracts/common'; import type { PopulatedTransaction, BigNumberish, Signer } from 'ethers'; -import { Utils } from 'radicle-drips'; +import Utils from '../utils'; import type { IERC20 } from '../../contracts'; import { IERC20__factory } from '../../contracts/factories'; import { validateClientSigner } from '../common/validators'; interface IERC20TxFactory extends Pick {} +/** + * A factory for creating `IERC20` contract transactions. + */ export default class ERC20TxFactory implements IERC20TxFactory { #erc20!: IERC20; #tokenAddress!: string; diff --git a/tests/AddressDriver/AddressDriverClient.integration.tests.ts b/tests/AddressDriver/AddressDriverClient.integration.tests.ts index f649b578..2e02ef71 100644 --- a/tests/AddressDriver/AddressDriverClient.integration.tests.ts +++ b/tests/AddressDriver/AddressDriverClient.integration.tests.ts @@ -33,7 +33,7 @@ describe('AddressDriver integration tests', () => { account2AddressDriverClient = await AddressDriverClient.create(provider, account2AsSigner); }); - it.only('should set Drips configuration', async () => { + it('should set Drips configuration', async () => { console.log(`Will update WETH (${WETH}) Drips configuration for ${account2}.`); const userId1 = await account1AddressDriverClient.getUserId(); diff --git a/tests/AddressDriver/AddressDriverClient.tests.ts b/tests/AddressDriver/AddressDriverClient.tests.ts index cdbf171c..3032044c 100644 --- a/tests/AddressDriver/AddressDriverClient.tests.ts +++ b/tests/AddressDriver/AddressDriverClient.tests.ts @@ -4,7 +4,6 @@ import { assert } from 'chai'; import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import type { Network } from '@ethersproject/networks'; import { BigNumber, constants, ethers, Wallet } from 'ethers'; -import { AddressDriverTxFactory } from 'radicle-drips'; import type { AddressDriver, IERC20 } from '../../contracts'; import { IERC20__factory, AddressDriver__factory } from '../../contracts'; import type { SplitsReceiverStruct, DripsReceiverStruct, UserMetadata } from '../../src/common/types'; @@ -14,6 +13,7 @@ import { DripsErrorCode } from '../../src/common/DripsError'; import * as validators from '../../src/common/validators'; import DripsHubClient from '../../src/DripsHub/DripsHubClient'; import * as internals from '../../src/common/internals'; +import AddressDriverTxFactory from '../../src/AddressDriver/AddressDriverTxFactory'; describe('AddressDriverClient', () => { const TEST_CHAIN_ID = 5; // Goerli. @@ -54,7 +54,7 @@ describe('AddressDriverClient', () => { addressDriverTxFactoryStub = stubInterface(); sinon .stub(AddressDriverTxFactory, 'create') - .withArgs(providerStub, Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER) + .withArgs(signerStub, Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER) .resolves(addressDriverTxFactoryStub); testAddressDriverClient = await AddressDriverClient.create( @@ -70,51 +70,34 @@ describe('AddressDriverClient', () => { }); describe('create()', () => { - it('should validate the signer', async () => { + it('should validate the provider', async () => { // Arrange - const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); + const validateClientProviderStub = sinon.stub(validators, 'validateClientProvider'); // Act await AddressDriverClient.create(providerStub, signerStub); // Assert assert( - validateClientSignerStub.calledWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), + validateClientProviderStub.calledWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); - it('should validate the provider', async () => { + it('should validate the signer', async () => { // Arrange - const validateClientProviderStub = sinon.stub(validators, 'validateClientProvider'); + const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); // Act await AddressDriverClient.create(providerStub, signerStub); // Assert assert( - validateClientProviderStub.calledWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), + validateClientSignerStub.calledWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); - it('should should throw a initializationError when client cannot be initialized', async () => { - // Arrange - let threw = false; - - try { - // Act - await AddressDriverClient.create(undefined as any, undefined as any); - } catch (error: any) { - // Assert - assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); - threw = true; - } - - // Assert - assert.isTrue(threw, 'Expected type of exception was not thrown'); - }); - it('should set the custom driver address when provided', async () => { // Arrange const customDriverAddress = Wallet.createRandom().address; @@ -364,19 +347,19 @@ describe('AddressDriverClient', () => { ); }); - it('should call the collect() method of the AddressDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const tokenAddress = Wallet.createRandom().address; const transferToAddress = Wallet.createRandom().address; + const tx = {}; + addressDriverTxFactoryStub.collect.withArgs(tokenAddress, transferToAddress).resolves(tx); + // Act await testAddressDriverClient.collect(tokenAddress, transferToAddress); // Assert - assert( - addressDriverContractStub.collect.calledOnceWithExactly(tokenAddress, transferToAddress), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -444,20 +427,20 @@ describe('AddressDriverClient', () => { assert(validateAddressStub.calledOnceWithExactly(tokenAddress)); }); - it('should call the give() method of the AddressDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const amount = 100n; const receiverUserId = '1'; const tokenAddress = Wallet.createRandom().address; + const tx = {}; + addressDriverTxFactoryStub.give.withArgs(receiverUserId, tokenAddress, amount).resolves(tx); + // Act await testAddressDriverClient.give(receiverUserId, tokenAddress, amount); // Assert - assert( - addressDriverContractStub.give.calledOnceWithExactly(receiverUserId, tokenAddress, amount), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -492,27 +475,21 @@ describe('AddressDriverClient', () => { assert(validateSplitsReceiversStub.calledOnceWithExactly(receivers)); }); - it('should call the setSplits() method of the AddressDriver contract', async () => { - // Arrange + it('should send the expected transaction', async () => { const receivers: SplitsReceiverStruct[] = [ { userId: 2, weight: 100 }, { userId: 1, weight: 1 }, { userId: 1, weight: 1 } ]; + const tx = {}; + addressDriverTxFactoryStub.setSplits.withArgs(receivers).resolves(tx); + // Act await testAddressDriverClient.setSplits(receivers); // Assert - assert( - addressDriverContractStub.setSplits.calledOnceWithExactly( - sinon - .match((r: SplitsReceiverStruct[]) => r.length === 2) - .and(sinon.match((r: SplitsReceiverStruct[]) => r[0].userId === 1)) - .and(sinon.match((r: SplitsReceiverStruct[]) => r[1].userId === 2)) - ), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -623,14 +600,18 @@ describe('AddressDriverClient', () => { } ]; + const balance = 1n; + const tx = {}; - addressDriverTxFactoryStub.setDrips.resolves(tx); + addressDriverTxFactoryStub.setDrips + .withArgs(tokenAddress, currentReceivers, balance, receivers, 0, 0, transferToAddress) + .resolves(tx); // Act - await testAddressDriverClient.setDrips(tokenAddress, currentReceivers, receivers, transferToAddress, 1n); + await testAddressDriverClient.setDrips(tokenAddress, currentReceivers, receivers, transferToAddress, balance); // Assert - assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Expected method to be called'); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -744,19 +725,19 @@ describe('AddressDriverClient', () => { assert(validateEmitUserMetadataInputStub.calledOnceWithExactly(metadata)); }); - it('should call the emitUserMetadata() method of the AddressDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const metadata: UserMetadata[] = [{ key: 'key', value: 'value' }]; const metadataAsBytes = metadata.map((m) => internals.createFromStrings(m.key, m.value)); + const tx = {}; + addressDriverTxFactoryStub.emitUserMetadata.withArgs(metadataAsBytes).resolves(tx); + // Act await testAddressDriverClient.emitUserMetadata(metadata); // Assert - assert( - addressDriverContractStub.emitUserMetadata.calledOnceWithExactly(metadataAsBytes), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); }); diff --git a/tests/AddressDriver/AddressDriverTxFactory.tests.ts b/tests/AddressDriver/AddressDriverTxFactory.tests.ts index 6ffce91c..012a4d28 100644 --- a/tests/AddressDriver/AddressDriverTxFactory.tests.ts +++ b/tests/AddressDriver/AddressDriverTxFactory.tests.ts @@ -82,6 +82,7 @@ describe('AddressDriverTxFactory', () => { testAddressDriverTxFactory.driverAddress, Utils.Network.configs[(await providerStub.getNetwork()).chainId].CONTRACT_ADDRESS_DRIVER ); + assert.equal(testAddressDriverTxFactory.signer, signerWithProviderStub); }); }); From 428dd798aab1c135a4fad98a22797ffec377a484 Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Tue, 14 Mar 2023 10:48:26 +0100 Subject: [PATCH 5/6] refactor(nft driver client): reuse tx factories instead of the generated API --- src/AddressDriver/AddressDriverTxFactory.ts | 24 +- src/NFTDriver/NFTDriverClient.ts | 207 +++++------ src/NFTDriver/NFTDriverTxFactory.ts | 126 +++++-- .../AddressDriverClient.tests.ts | 16 - .../AddressDriverTxFactory.tests.ts | 20 +- tests/NFTDriver/NFTDriverClient.tests.ts | 340 ++++-------------- tests/NFTDriver/NFTDriverTxFactory.tests.ts | 100 ++++-- 7 files changed, 368 insertions(+), 465 deletions(-) diff --git a/src/AddressDriver/AddressDriverTxFactory.ts b/src/AddressDriver/AddressDriverTxFactory.ts index f090eac9..203df133 100644 --- a/src/AddressDriver/AddressDriverTxFactory.ts +++ b/src/AddressDriver/AddressDriverTxFactory.ts @@ -67,21 +67,26 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { public async collect( erc20: PromiseOrValue, - transferTo: PromiseOrValue + transferTo: PromiseOrValue, + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { - return this.#driver.populateTransaction.collect(erc20, transferTo); + return this.#driver.populateTransaction.collect(erc20, transferTo, overrides); } public async give( receiver: PromiseOrValue, erc20: PromiseOrValue, - amt: PromiseOrValue + amt: PromiseOrValue, + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { - return this.#driver.populateTransaction.give(receiver, erc20, amt); + return this.#driver.populateTransaction.give(receiver, erc20, amt, overrides); } - public async setSplits(receivers: SplitsReceiverStruct[]): Promise { - return this.#driver.populateTransaction.setSplits(formatSplitReceivers(receivers)); + public async setSplits( + receivers: SplitsReceiverStruct[], + overrides: Overrides & { from?: PromiseOrValue } = {} + ): Promise { + return this.#driver.populateTransaction.setSplits(formatSplitReceivers(receivers), overrides); } public async setDrips( @@ -123,7 +128,10 @@ export default class AddressDriverTxFactory implements IAddressDriverTxFactory { ); } - public async emitUserMetadata(userMetadata: UserMetadataStruct[]): Promise { - return this.#driver.populateTransaction.emitUserMetadata(userMetadata); + public async emitUserMetadata( + userMetadata: UserMetadataStruct[], + overrides: Overrides & { from?: PromiseOrValue } = {} + ): Promise { + return this.#driver.populateTransaction.emitUserMetadata(userMetadata, overrides); } } diff --git a/src/NFTDriver/NFTDriverClient.ts b/src/NFTDriver/NFTDriverClient.ts index 2f941682..6c0a7b2c 100644 --- a/src/NFTDriver/NFTDriverClient.ts +++ b/src/NFTDriver/NFTDriverClient.ts @@ -1,37 +1,35 @@ +/* eslint-disable no-dupe-class-members */ import type { Provider } from '@ethersproject/providers'; import type { BigNumberish, ContractTransaction, Signer } from 'ethers'; import { constants, BigNumber } from 'ethers'; import type { DripsReceiverStruct, SplitsReceiverStruct, UserMetadata } from '../common/types'; import type { NFTDriver } from '../../contracts'; -import { IERC20__factory, NFTDriver__factory } from '../../contracts'; +import { NFTDriver__factory, IERC20__factory } from '../../contracts'; import { DripsErrors } from '../common/DripsError'; import { validateAddress, validateClientProvider, validateClientSigner, - validateDripsReceivers, validateEmitUserMetadataInput, + validateSetDripsInput, validateSplitsReceivers } from '../common/validators'; import Utils from '../utils'; -import { - createFromStrings, - formatDripsReceivers, - formatSplitReceivers, - isNullOrUndefined, - nameOf -} from '../common/internals'; +import { createFromStrings, isNullOrUndefined, nameOf } from '../common/internals'; import dripsConstants from '../constants'; +import type { INFTDriverTxFactory } from './NFTDriverTxFactory'; +import NFTDriverTxFactory from './NFTDriverTxFactory'; + /** * A client for managing Drips accounts identified by NFTs. * @see {@link https://github.com/radicle-dev/drips-contracts/blob/master/src/NFTDriver.sol NFTDriver} contract. */ export default class NFTDriverClient { - #driver!: NFTDriver; #signer!: Signer; - #signerAddress!: string; - #driverAddress!: string; + #driver!: NFTDriver; #provider!: Provider; + #driverAddress!: string; + #txFactory!: INFTDriverTxFactory; /** Returns the client's `provider`. */ public get provider(): Provider { @@ -57,15 +55,15 @@ export default class NFTDriverClient { // TODO: Update the supported chains documentation comments. /** * Creates a new immutable `NFTDriverClient` instance. - * @param {Provider} provider The network provider. It cannot be changed after creation. + * @param provider The network provider. It cannot be changed after creation. * * The `provider` must be connected to one of the following supported networks: * - 'goerli': chain ID `5` * - 'polygon-mumbai': chain ID `80001` - * @param {Signer} signer The singer used to sign transactions. It cannot be changed after creation. + * @param signer The singer used to sign transactions. It cannot be changed after creation. * * **Important**: If the `signer` is _not_ connected to a provider it will try to connect to the `provider`, else it will use the `signer.provider`. - * @param {string|undefined} customDriverAddress Overrides the `NFTDriver` contract address. + * @param customDriverAddress Overrides the `NFTDriver` contract address. * If it's `undefined` (default value), the address will be automatically selected based on the `provider`'s network. * @returns A `Promise` which resolves to the new client instance. * @throws {@link DripsErrors.initializationError} if the client initialization fails. @@ -74,37 +72,45 @@ export default class NFTDriverClient { provider: Provider, signer: Signer, customDriverAddress?: string + ): Promise; + public static async create( + provider: Provider, + signer: Signer, + customDriverAddress?: string, + txFactory?: INFTDriverTxFactory + ): Promise; + public static async create( + provider: Provider, + signer: Signer, + customDriverAddress?: string, + txFactory?: INFTDriverTxFactory ): Promise { - try { - await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); + await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); - if (!signer.provider) { - // eslint-disable-next-line no-param-reassign - signer = signer.connect(provider); - } + if (!signer.provider) { + // eslint-disable-next-line no-param-reassign + signer = signer.connect(provider); + } - await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); - const network = await provider.getNetwork(); - const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_NFT_DRIVER; + const network = await provider.getNetwork(); + const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_NFT_DRIVER; - const client = new NFTDriverClient(); + const client = new NFTDriverClient(); - client.#signer = signer; - client.#provider = provider; - client.#driverAddress = driverAddress; - client.#signerAddress = await signer.getAddress(); - client.#driver = NFTDriver__factory.connect(driverAddress, signer); + client.#signer = signer; + client.#provider = provider; + client.#driverAddress = driverAddress; + client.#driver = NFTDriver__factory.connect(driverAddress, signer); + client.#txFactory = txFactory || (await NFTDriverTxFactory.create(signer, customDriverAddress)); - return client; - } catch (error: any) { - throw DripsErrors.initializationError(`Could not create 'NFTDriverClient': ${error.message}`); - } + return client; } /** * Returns the remaining number of tokens the `NFTDriver` contract is allowed to spend on behalf of the client's `signer` for the given ERC20 token. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. @@ -119,14 +125,16 @@ export default class NFTDriverClient { const signerAsErc20Contract = IERC20__factory.connect(tokenAddress, this.#signer); - const allowance = await signerAsErc20Contract.allowance(this.#signerAddress, this.#driverAddress); + const signerAddress = await this.#signer.getAddress(); + + const allowance = await signerAsErc20Contract.allowance(signerAddress, this.#driverAddress); return allowance.toBigInt(); } /** * Sets the maximum allowance value for the `NFTDriver` contract over the client's `signer` tokens for the given ERC20 token. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. @@ -158,13 +166,13 @@ export default class NFTDriverClient { * The minted NFT's ID (token ID) and the user ID controlled by it are always equal. * * This means that **anywhere in the SDK, a method expects a user ID parameter, and a token ID is a valid argument**. - * @param {string} transferToAddress The address to transfer the minted token to. - * @param {string} associatedApp + * @param transferToAddress The address to transfer the minted token to. + * @param associatedApp * The name/ID of the app that is associated with the new account. * If provided, the following user metadata entry will be appended to the `userMetadata` list: * - key: "associatedApp" * - value: `associatedApp`. - * @param {UserMetadata[]} userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. + * @param userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. * * **Tip**: you might want to use `Utils.UserMetadata.createFromStrings` to easily create metadata instances from `string` inputs. * @returns A `Promise` which resolves to minted token ID. It's equal to the user ID controlled by it. @@ -202,13 +210,13 @@ export default class NFTDriverClient { * The minted NFT's ID (token ID) and the user ID controlled by it are always equal. * * This means that **anywhere in the SDK, a method expects a user ID parameter, and a token ID is a valid argument**. - * @param {string} transferToAddress The address to transfer the minted token to. - * @param {string} associatedApp + * @param transferToAddress The address to transfer the minted token to. + * @param associatedApp * The name/ID of the app that is associated with the new account. * If provided, the following user metadata entry will be appended to the `userMetadata` list: * - key: "associatedApp" * - value: `associatedApp`. - * @param {UserMetadata[]} userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. + * @param userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. * * **Tip**: you might want to use `Utils.UserMetadata.createFromStrings` to easily create metadata instances from `string` inputs. * @returns A `Promise` which resolves to minted token ID. It's equal to the user ID controlled by it. @@ -239,15 +247,15 @@ export default class NFTDriverClient { * Collects the received and already split funds and transfers them from the `DripsHub` contract to an address. * * The caller (client's `signer`) must be the owner of the `tokenId` or be approved to use it. - * @param {string} tokenId The ID of the token representing the collecting account. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenId The ID of the token representing the collecting account. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. * Tokens which rebase the holders' balances, collect taxes on transfers, * or impose any restrictions on holding or transferring tokens are not supported. * If you use such tokens in the protocol, they can get stuck or lost. - * @param {string} transferToAddress The address to send collected funds to. + * @param transferToAddress The address to send collected funds to. * @returns A `Promise` which resolves to the contract transaction. * @throws {@link DripsErrors.argumentMissingError} if the `tokenId` is missing. * @throws {@link DripsErrors.addressError} if `tokenAddress` or `transferToAddress` is not valid. @@ -263,7 +271,9 @@ export default class NFTDriverClient { validateAddress(tokenAddress); validateAddress(transferToAddress); - return this.#driver.collect(tokenId, tokenAddress, transferToAddress); + const tx = await this.#txFactory.collect(tokenId, tokenAddress, transferToAddress); + + return this.#signer.sendTransaction(tx); } /** @@ -271,22 +281,22 @@ export default class NFTDriverClient { * The receiver can collect them immediately. * * The caller (client's `signer`) must be the owner of the `tokenId` or be approved to use it. - * @param {string} tokenId The ID of the token representing the giving account. - * @param {string} receiverUserId The receiver user ID. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenId The ID of the token representing the giving account. + * @param receiverUserId The receiver user ID. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. * Tokens which rebase the holders' balances, collect taxes on transfers, * or impose any restrictions on holding or transferring tokens are not supported. * If you use such tokens in the protocol, they can get stuck or lost. - * @param {BigNumberish} amount The amount to give (in the smallest unit, e.g., Wei). It must be greater than `0`. + * @param amount The amount to give (in the smallest unit, e.g., Wei). It must be greater than `0`. * @returns A `Promise` which resolves to the contract transaction. * @throws {@link DripsErrors.argumentMissingError} if any of the required parameters is missing. * @throws {@link DripsErrors.addressError} if the `tokenAddress` is not valid. * @throws {@link DripsErrors.argumentError} if the `amount` is less than or equal to `0`. */ - public give( + public async give( tokenId: string, receiverUserId: string, tokenAddress: string, @@ -316,7 +326,9 @@ export default class NFTDriverClient { validateAddress(tokenAddress); - return this.#driver.give(tokenId, receiverUserId, tokenAddress, amount); + const tx = await this.#txFactory.give(tokenId, receiverUserId, tokenAddress, amount); + + return this.#signer.sendTransaction(tx); } /** @@ -325,23 +337,23 @@ export default class NFTDriverClient { * It will transfer funds from the client's `signer` wallet to the `DripsHub` contract to fulfill the change of the drips balance. * * The caller (client's `signer`) must be the owner of the `tokenId` or be approved to use it. - * @param {string} tokenId The ID of the token representing the configured account. - * @param {string} tokenAddress The ERC20 token address. + * @param tokenId The ID of the token representing the configured account. + * @param tokenAddress The ERC20 token address. * * It must preserve amounts, so if some amount of tokens is transferred to * an address, then later the same amount must be transferrable from that address. * Tokens which rebase the holders' balances, collect taxes on transfers, * or impose any restrictions on holding or transferring tokens are not supported. * If you use such tokens in the protocol, they can get stuck or lost. - * @param {DripsReceiverStruct[]} currentReceivers The drips receivers that were set in the last drips update. + * @param currentReceivers The drips receivers that were set in the last drips update. * Pass an empty array if this is the first update. * * **Tip**: you might want to use `DripsSubgraphClient.getCurrentDripsReceivers` to easily retrieve the list of current receivers. - * @param {DripsReceiverStruct[]} newReceivers The new drips receivers (max `100`). + * @param newReceivers The new drips receivers (max `100`). * Duplicate receivers are not allowed and will only be processed once. * Pass an empty array if you want to clear all receivers. - * @param {string} transferToAddress The address to send funds to in case of decreasing balance. - * @param {BigNumberish} balanceDelta The drips balance change to be applied: + * @param transferToAddress The address to send funds to in case of decreasing balance. + * @param balanceDelta The drips balance change to be applied: * - Positive to add funds to the drips balance. * - Negative to remove funds from the drips balance. * - `0` to leave drips balance as is (default value). @@ -367,69 +379,40 @@ export default class NFTDriverClient { nameOf({ tokenId }) ); } - - validateAddress(tokenAddress); - - validateDripsReceivers( - currentReceivers.map((r) => ({ + validateSetDripsInput( + tokenAddress, + currentReceivers?.map((r) => ({ userId: r.userId.toString(), config: Utils.DripsReceiverConfiguration.fromUint256(BigNumber.from(r.config).toBigInt()) - })) - ); - - validateDripsReceivers( - newReceivers.map((r) => ({ + })), + newReceivers?.map((r) => ({ userId: r.userId.toString(), config: Utils.DripsReceiverConfiguration.fromUint256(BigNumber.from(r.config).toBigInt()) - })) + })), + transferToAddress, + balanceDelta ); - if (!transferToAddress) { - throw DripsErrors.argumentMissingError( - `Could not set drips: '${nameOf({ transferToAddress })}' is missing.`, - nameOf({ transferToAddress }) - ); - } - - const formattedCurrentReceivers = formatDripsReceivers(currentReceivers); - const formattedNewReceivers = formatDripsReceivers(newReceivers); - - const estimatedGasFees = ( - await this.#driver.estimateGas.setDrips( - tokenId, - tokenAddress, - formattedCurrentReceivers, - balanceDelta, - formattedNewReceivers, - 0, - 0, - transferToAddress - ) - ).toNumber(); - - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - return this.#driver.setDrips( + const tx = await this.#txFactory.setDrips( tokenId, tokenAddress, - formattedCurrentReceivers, + currentReceivers, balanceDelta, - formattedNewReceivers, + newReceivers, 0, 0, - transferToAddress, - { - gasLimit - } + transferToAddress ); + + return this.#signer.sendTransaction(tx); } /** * Sets the account's Splits configuration. * * The caller (client's `signer`) must be the owner of the `tokenId` or be approved to use it. - * @param {string} tokenId The ID of the token representing the configured account. - * @param {SplitsReceiverStruct[]} receivers The splits receivers (max `200`). + * @param tokenId The ID of the token representing the configured account. + * @param receivers The splits receivers (max `200`). * Each splits receiver will be getting `weight / TOTAL_SPLITS_WEIGHT` share of the funds. * Duplicate receivers are not allowed and will only be processed once. * Pass an empty array if you want to clear all receivers. @@ -438,7 +421,7 @@ export default class NFTDriverClient { * @throws {@link DripsErrors.argumentError} if `receivers`' count exceeds the max allowed splits receivers. * @throws {@link DripsErrors.splitsReceiverError} if any of the `receivers` is not valid. */ - public setSplits(tokenId: string, receivers: SplitsReceiverStruct[]): Promise { + public async setSplits(tokenId: string, receivers: SplitsReceiverStruct[]): Promise { if (isNullOrUndefined(tokenId)) { throw DripsErrors.argumentMissingError( `Could not set splits: '${nameOf({ tokenId })}' is missing.`, @@ -448,7 +431,9 @@ export default class NFTDriverClient { validateSplitsReceivers(receivers); - return this.#driver.setSplits(tokenId, formatSplitReceivers(receivers)); + const tx = await this.#txFactory.setSplits(tokenId, receivers); + + return this.#signer.sendTransaction(tx); } /** @@ -456,14 +441,14 @@ export default class NFTDriverClient { * The key and the value are _not_ standardized by the protocol, it's up to the caller to establish and follow conventions to ensure compatibility with the consumers. * * The caller (client's `signer`) must be the owner of the `tokenId` or be approved to use it. - * @param {string} tokenId The ID of the token representing the emitting account. - * @param {UserMetadata[]} userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. + * @param tokenId The ID of the token representing the emitting account. + * @param userMetadata The list of user metadata. Note that a metadata `key` needs to be 32bytes. * * **Tip**: you might want to use `Utils.UserMetadata.createFromStrings` to easily create metadata instances from `string` inputs. * @returns A `Promise` which resolves to the contract transaction. * @throws {@link DripsErrors.argumentError} if any of the metadata entries is not valid. */ - public emitUserMetadata(tokenId: string, userMetadata: UserMetadata[]): Promise { + public async emitUserMetadata(tokenId: string, userMetadata: UserMetadata[]): Promise { if (!tokenId) { throw DripsErrors.argumentError(`Could not emit user metadata: '${nameOf({ tokenId })}' is missing.`); } @@ -472,7 +457,9 @@ export default class NFTDriverClient { const userMetadataAsBytes = userMetadata.map((m) => createFromStrings(m.key, m.value)); - return this.#driver.emitUserMetadata(tokenId, userMetadataAsBytes); + const tx = await this.#txFactory.emitUserMetadata(tokenId, userMetadataAsBytes); + + return this.#signer.sendTransaction(tx); } async #getTokenIdFromTxResponse(txResponse: ContractTransaction) { diff --git a/src/NFTDriver/NFTDriverTxFactory.ts b/src/NFTDriver/NFTDriverTxFactory.ts index 37c5d018..112cff9a 100644 --- a/src/NFTDriver/NFTDriverTxFactory.ts +++ b/src/NFTDriver/NFTDriverTxFactory.ts @@ -1,19 +1,20 @@ /* eslint-disable no-dupe-class-members */ -import type { Provider } from '@ethersproject/providers'; import type { NFTDriver, DripsReceiverStruct, SplitsReceiverStruct, UserMetadataStruct } from 'contracts/NFTDriver'; import type { PromiseOrValue } from 'contracts/common'; -import type { PopulatedTransaction, BigNumberish } from 'ethers'; +import type { PopulatedTransaction, BigNumberish, Signer, Overrides } from 'ethers'; +import { formatDripsReceivers, formatSplitReceivers } from '../common/internals'; import { NFTDriver__factory } from '../../contracts/factories'; -import { validateClientProvider } from '../common/validators'; +import { validateClientSigner } from '../common/validators'; import Utils from '../utils'; -interface INFTDriverTxFactory +export interface INFTDriverTxFactory extends Pick< NFTDriver['populateTransaction'], - 'safeMint' | 'collect' | 'give' | 'setSplits' | 'setDrips' | 'emitUserMetadata' + 'mint' | 'safeMint' | 'collect' | 'give' | 'setSplits' | 'setDrips' | 'emitUserMetadata' > {} export default class NFTDriverTxFactory implements INFTDriverTxFactory { + #signer!: Signer; #driver!: NFTDriver; #driverAddress!: string; @@ -21,70 +22,131 @@ export default class NFTDriverTxFactory implements INFTDriverTxFactory { return this.#driverAddress; } - public static async create(provider: Provider, customDriverAddress?: string): Promise { - await validateClientProvider(provider, Utils.Network.SUPPORTED_CHAINS); + public get signer(): Signer | undefined { + return this.#signer; + } - const network = await provider.getNetwork(); - const driverAddress = customDriverAddress ?? Utils.Network.configs[network.chainId].CONTRACT_ADDRESS_DRIVER; + // TODO: Update the supported chains documentation comments. + /** + * Creates a new immutable `NFTDriverTxFactory` instance. + * + * @param signer The signer that will be used to sign the generated transactions. + * + * The `singer` must be connected to a provider. + * + * The supported networks are: + * - 'goerli': chain ID `5` + * - 'polygon-mumbai': chain ID `80001` + * @param customDriverAddress Overrides the `NFTDriver` contract address. + * If it's `undefined` (default value), the address will be automatically selected based on the `signer.provider`'s network. + * @returns A `Promise` which resolves to the new client instance. + * @throws {@link DripsErrors.initializationError} if the initialization fails. + */ + public static async create(signer: Signer, customDriverAddress?: string): Promise { + await validateClientSigner(signer, Utils.Network.SUPPORTED_CHAINS); - const client = new NFTDriverTxFactory(); + const { chainId } = await signer.provider!.getNetwork(); // If the validation passed we know that the signer is connected to a provider. - client.#driverAddress = driverAddress; + const driverAddress = customDriverAddress || Utils.Network.configs[chainId].CONTRACT_NFT_DRIVER; - client.#driver = NFTDriver__factory.connect(driverAddress, provider); + const client = new NFTDriverTxFactory(); + client.#signer = signer; + client.#driverAddress = driverAddress; + client.#driver = NFTDriver__factory.connect(driverAddress, signer); return client; } - safeMint(to: PromiseOrValue, userMetadata: UserMetadataStruct[]): Promise { - return this.#driver.populateTransaction.safeMint(to, userMetadata); + public async mint( + to: PromiseOrValue, + userMetadata: UserMetadataStruct[], + overrides: Overrides & { from?: PromiseOrValue } = {} + ): Promise { + return this.#driver.populateTransaction.mint(to, userMetadata, overrides); } - collect( + public async safeMint( + to: PromiseOrValue, + userMetadata: UserMetadataStruct[], + overrides: Overrides & { from?: PromiseOrValue } = {} + ): Promise { + return this.#driver.populateTransaction.safeMint(to, userMetadata, overrides); + } + + public async collect( tokenId: PromiseOrValue, erc20: PromiseOrValue, - transferTo: PromiseOrValue + transferTo: PromiseOrValue, + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { - return this.#driver.populateTransaction.collect(tokenId, erc20, transferTo); + return this.#driver.populateTransaction.collect(tokenId, erc20, transferTo, overrides); } - give( + public async give( tokenId: PromiseOrValue, receiver: PromiseOrValue, erc20: PromiseOrValue, - amt: PromiseOrValue + amt: PromiseOrValue, + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { - return this.#driver.populateTransaction.give(tokenId, receiver, erc20, amt); + return this.#driver.populateTransaction.give(tokenId, receiver, erc20, amt, overrides); } - setSplits(tokenId: PromiseOrValue, receivers: SplitsReceiverStruct[]): Promise { - return this.#driver.populateTransaction.setSplits(tokenId, receivers); + public async setSplits( + tokenId: PromiseOrValue, + receivers: SplitsReceiverStruct[], + overrides: Overrides & { from?: PromiseOrValue } = {} + ): Promise { + return this.#driver.populateTransaction.setSplits(tokenId, formatSplitReceivers(receivers), overrides); } - setDrips( + public async setDrips( tokenId: PromiseOrValue, erc20: PromiseOrValue, currReceivers: DripsReceiverStruct[], balanceDelta: PromiseOrValue, newReceivers: DripsReceiverStruct[], - transferTo: PromiseOrValue + maxEndHint1: PromiseOrValue, + maxEndHint2: PromiseOrValue, + transferTo: PromiseOrValue, + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { + if (!overrides.gasLimit) { + const gasEstimation = await this.#driver.estimateGas.setDrips( + tokenId, + erc20, + formatDripsReceivers(currReceivers), + balanceDelta, + formatDripsReceivers(newReceivers), + maxEndHint1, + maxEndHint2, + transferTo, + overrides + ); + + const gasLimit = Math.ceil(gasEstimation.toNumber() * 1.2); + // eslint-disable-next-line no-param-reassign + overrides = { ...overrides, gasLimit }; + } + return this.#driver.populateTransaction.setDrips( tokenId, erc20, - currReceivers, + formatDripsReceivers(currReceivers), balanceDelta, - newReceivers, - 0, - 0, - transferTo + formatDripsReceivers(newReceivers), + maxEndHint1, + maxEndHint2, + transferTo, + overrides ); } - emitUserMetadata( + public async emitUserMetadata( tokenId: PromiseOrValue, - userMetadata: UserMetadataStruct[] + userMetadata: UserMetadataStruct[], + overrides: Overrides & { from?: PromiseOrValue } = {} ): Promise { - return this.#driver.populateTransaction.emitUserMetadata(tokenId, userMetadata); + return this.#driver.populateTransaction.emitUserMetadata(tokenId, userMetadata, overrides); } } diff --git a/tests/AddressDriver/AddressDriverClient.tests.ts b/tests/AddressDriver/AddressDriverClient.tests.ts index 3032044c..becd1b3b 100644 --- a/tests/AddressDriver/AddressDriverClient.tests.ts +++ b/tests/AddressDriver/AddressDriverClient.tests.ts @@ -11,7 +11,6 @@ import AddressDriverClient from '../../src/AddressDriver/AddressDriverClient'; import Utils from '../../src/utils'; import { DripsErrorCode } from '../../src/common/DripsError'; import * as validators from '../../src/common/validators'; -import DripsHubClient from '../../src/DripsHub/DripsHubClient'; import * as internals from '../../src/common/internals'; import AddressDriverTxFactory from '../../src/AddressDriver/AddressDriverTxFactory'; @@ -20,7 +19,6 @@ describe('AddressDriverClient', () => { let networkStub: StubbedInstance; let signerStub: StubbedInstance; - let dripsHubClientStub: StubbedInstance; let providerStub: sinon.SinonStubbedInstance; let addressDriverContractStub: StubbedInstance; let addressDriverTxFactoryStub: StubbedInstance; @@ -48,9 +46,6 @@ describe('AddressDriverClient', () => { .withArgs(Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER, signerWithProviderStub) .returns(addressDriverContractStub); - dripsHubClientStub = stubInterface(); - sinon.stub(DripsHubClient, 'create').resolves(dripsHubClientStub); - addressDriverTxFactoryStub = stubInterface(); sinon .stub(AddressDriverTxFactory, 'create') @@ -500,11 +495,6 @@ describe('AddressDriverClient', () => { const transferToAddress = Wallet.createRandom().address; const ensureSignerExistsStub = sinon.stub(internals, 'ensureSignerExists'); - const estimatedGasFees = 2000000; - - addressDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; // Act await testAddressDriverClient.setDrips(tokenAddress, [], [], transferToAddress, undefined as unknown as bigint); @@ -543,12 +533,6 @@ describe('AddressDriverClient', () => { const validateSetDripsInputStub = sinon.stub(validators, 'validateSetDripsInput'); - const estimatedGasFees = 2000000; - - addressDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - // Act await testAddressDriverClient.setDrips(tokenAddress, currentReceivers, receivers, transferToAddress, 1n); diff --git a/tests/AddressDriver/AddressDriverTxFactory.tests.ts b/tests/AddressDriver/AddressDriverTxFactory.tests.ts index 012a4d28..690ecf1c 100644 --- a/tests/AddressDriver/AddressDriverTxFactory.tests.ts +++ b/tests/AddressDriver/AddressDriverTxFactory.tests.ts @@ -91,12 +91,13 @@ describe('AddressDriverTxFactory', () => { // Arrange const stub = sinon.stub(); addressDriverContractStub.populateTransaction.collect = stub; + const overrides = {}; // Act - await testAddressDriverTxFactory.collect('0x1234', '0x5678'); + await testAddressDriverTxFactory.collect('0x1234', '0x5678', overrides); // Assert - assert(stub.calledOnceWithExactly('0x1234', '0x5678')); + assert(stub.calledOnceWithExactly('0x1234', '0x5678', overrides)); }); }); @@ -105,12 +106,13 @@ describe('AddressDriverTxFactory', () => { // Arrange const stub = sinon.stub(); addressDriverContractStub.populateTransaction.give = stub; + const overrides = {}; // Act - await testAddressDriverTxFactory.give('0x1234', '0x5678', '0x9abc'); + await testAddressDriverTxFactory.give('0x1234', '0x5678', '0x9abc', overrides); // Assert - assert(stub.calledOnceWithExactly('0x1234', '0x5678', '0x9abc')); + assert(stub.calledOnceWithExactly('0x1234', '0x5678', '0x9abc', overrides)); }); }); @@ -120,12 +122,13 @@ describe('AddressDriverTxFactory', () => { const stub = sinon.stub(); addressDriverContractStub.populateTransaction.setSplits = stub; const receivers = [] as SplitsReceiverStruct[]; + const overrides = {}; // Act - await testAddressDriverTxFactory.setSplits(receivers); + await testAddressDriverTxFactory.setSplits(receivers, overrides); // Assert - assert(stub.calledOnceWithExactly(receivers)); + assert(stub.calledOnceWithExactly(receivers, overrides)); }); }); @@ -164,12 +167,13 @@ describe('AddressDriverTxFactory', () => { const stub = sinon.stub(); addressDriverContractStub.populateTransaction.emitUserMetadata = stub; const userMetadata = [] as UserMetadataStruct[]; + const overrides = {}; // Act - await testAddressDriverTxFactory.emitUserMetadata(userMetadata); + await testAddressDriverTxFactory.emitUserMetadata(userMetadata, overrides); // Assert - assert(stub.calledOnceWithExactly(userMetadata)); + assert(stub.calledOnceWithExactly(userMetadata, overrides)); }); }); }); diff --git a/tests/NFTDriver/NFTDriverClient.tests.ts b/tests/NFTDriver/NFTDriverClient.tests.ts index 272cea09..e81b4327 100644 --- a/tests/NFTDriver/NFTDriverClient.tests.ts +++ b/tests/NFTDriver/NFTDriverClient.tests.ts @@ -2,18 +2,18 @@ import type { Network } from '@ethersproject/networks'; import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import type { StubbedInstance } from 'ts-sinon'; import sinon, { stubInterface, stubObject } from 'ts-sinon'; -import type { ContractReceipt, ContractTransaction, Event } from 'ethers'; +import type { ContractReceipt, ContractTransaction } from 'ethers'; import { ethers, BigNumber, constants, Wallet } from 'ethers'; import { assert } from 'chai'; -import type { IERC20, NFTDriver } from '../../contracts'; -import { IERC20__factory, NFTDriver__factory } from '../../contracts'; -import DripsHubClient from '../../src/DripsHub/DripsHubClient'; +import { DripsErrorCode } from 'radicle-drips'; import NFTDriverClient from '../../src/NFTDriver/NFTDriverClient'; import Utils from '../../src/utils'; -import { DripsErrorCode } from '../../src/common/DripsError'; -import * as internals from '../../src/common/internals'; import * as validators from '../../src/common/validators'; -import type { DripsReceiverStruct, SplitsReceiverStruct, DripsReceiver, UserMetadata } from '../../src/common/types'; +import NFTDriverTxFactory from '../../src/NFTDriver/NFTDriverTxFactory'; +import type { IERC20, NFTDriver } from '../../contracts'; +import { NFTDriver__factory, IERC20__factory } from '../../contracts'; +import type { DripsReceiverStruct, SplitsReceiverStruct, UserMetadata } from '../../src/common/types'; +import * as internals from '../../src/common/internals'; describe('NFTDriverClient', () => { const TEST_CHAIN_ID = 5; // Goerli. @@ -21,9 +21,9 @@ describe('NFTDriverClient', () => { let networkStub: StubbedInstance; let signerStub: StubbedInstance; let nftDriverContractStub: StubbedInstance; - let dripsHubClientStub: StubbedInstance; let signerWithProviderStub: StubbedInstance; let providerStub: sinon.SinonStubbedInstance; + let nftDriverTxFactoryStub: StubbedInstance; let testNftDriverClient: NFTDriverClient; @@ -41,16 +41,19 @@ describe('NFTDriverClient', () => { signerWithProviderStub = { ...signerStub, provider: providerStub }; signerStub.connect.withArgs(providerStub).returns(signerWithProviderStub); + nftDriverTxFactoryStub = stubInterface(); + sinon + .stub(NFTDriverTxFactory, 'create') + .withArgs(signerWithProviderStub, Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_NFT_DRIVER) + .resolves(nftDriverTxFactoryStub); + nftDriverContractStub = stubInterface(); sinon .stub(NFTDriver__factory, 'connect') .withArgs(Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_NFT_DRIVER, signerWithProviderStub) .returns(nftDriverContractStub); - dripsHubClientStub = stubInterface(); - sinon.stub(DripsHubClient, 'create').resolves(dripsHubClientStub); - - testNftDriverClient = await NFTDriverClient.create(providerStub, signerStub); + testNftDriverClient = await NFTDriverClient.create(providerStub, signerStub, undefined, nftDriverTxFactoryStub); }); afterEach(() => { @@ -58,51 +61,34 @@ describe('NFTDriverClient', () => { }); describe('create()', () => { - it('should validate the signer', async () => { + it('should validate the provider', async () => { // Arrange - const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); + const validateClientProviderStub = sinon.stub(validators, 'validateClientProvider'); // Act await NFTDriverClient.create(providerStub, signerStub); // Assert assert( - validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), + validateClientProviderStub.calledWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); - it('should validate the provider', async () => { + it('should validate the signer', async () => { // Arrange - const validateClientProviderStub = sinon.stub(validators, 'validateClientProvider'); + const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); // Act await NFTDriverClient.create(providerStub, signerStub); // Assert assert( - validateClientProviderStub.calledOnceWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), + validateClientSignerStub.calledWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); - it('should should throw a initializationError when client cannot be initialized', async () => { - // Arrange - let threw = false; - - try { - // Act - await NFTDriverClient.create(undefined as any, undefined as any); - } catch (error: any) { - // Assert - assert.equal(error.code, DripsErrorCode.INITIALIZATION_FAILURE); - threw = true; - } - - // Assert - assert.isTrue(threw, 'Expected type of exception was not thrown'); - }); - it('should set the custom driver address when provided', async () => { // Arrange const customDriverAddress = Wallet.createRandom().address; @@ -118,7 +104,7 @@ describe('NFTDriverClient', () => { // Assert assert.equal(testNftDriverClient.signer, signerWithProviderStub); assert.equal(testNftDriverClient.provider, providerStub); - assert.equal(testNftDriverClient.signer.provider, providerStub); + assert.equal(testNftDriverClient.signer!.provider, providerStub); assert.equal( testNftDriverClient.driverAddress, Utils.Network.configs[(await providerStub.getNetwork()).chainId].CONTRACT_NFT_DRIVER @@ -565,20 +551,20 @@ describe('NFTDriverClient', () => { ); }); - it('should call the collect() method of the NFTDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const tokenId = '1'; const tokenAddress = Wallet.createRandom().address; const transferToAddress = Wallet.createRandom().address; + const tx = {}; + nftDriverTxFactoryStub.collect.withArgs(tokenId, tokenAddress, transferToAddress).resolves(tx); + // Act await testNftDriverClient.collect(tokenId, tokenAddress, transferToAddress); // Assert - assert( - nftDriverContractStub.collect.calledOnceWithExactly(tokenId, tokenAddress, transferToAddress), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -650,21 +636,21 @@ describe('NFTDriverClient', () => { assert(validateAddressStub.calledOnceWithExactly(tokenAddress)); }); - it('should call the give() method of the NFTDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const tokenId = '1'; - const amount = 100; + const amount = 100n; const receiverUserId = '1'; const tokenAddress = Wallet.createRandom().address; + const tx = {}; + nftDriverTxFactoryStub.give.withArgs(tokenId, receiverUserId, tokenAddress, amount).resolves(tx); + // Act await testNftDriverClient.give(tokenId, receiverUserId, tokenAddress, amount); // Assert - assert( - nftDriverContractStub.give.calledOnceWithExactly(tokenId, receiverUserId, tokenAddress, amount), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -688,48 +674,7 @@ describe('NFTDriverClient', () => { assert.isTrue(threw, 'Expected type of exception was not thrown'); }); - it('should validate the ERC20 address', async () => { - // Arrange - const tokenId = '1'; - const tokenAddress = Wallet.createRandom().address; - const transferToAddress = Wallet.createRandom().address; - const currentReceivers: DripsReceiverStruct[] = [ - { - userId: 3, - config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 3n, duration: 3n, start: 3n }) - } - ]; - const receivers: DripsReceiverStruct[] = [ - { - userId: 2, - config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 1n, duration: 1n, start: 1n }) - }, - { - userId: 2, - config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 1n, duration: 1n, start: 1n }) - }, - { - userId: 1, - config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 2n, duration: 2n, start: 2n }) - } - ]; - - const estimatedGasFees = 2000000; - - nftDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - - const validateAddressStub = sinon.stub(validators, 'validateAddress'); - - // Act - await testNftDriverClient.setDrips(tokenId, tokenAddress, currentReceivers, receivers, transferToAddress, 1n); - - // Assert - assert(validateAddressStub.calledOnceWithExactly(tokenAddress)); - }); - - it('should validate the drips receivers', async () => { + it('should validate the input', async () => { // Arrange const tokenId = '1'; const tokenAddress = Wallet.createRandom().address; @@ -756,145 +701,35 @@ describe('NFTDriverClient', () => { } ]; - const estimatedGasFees = 2000000; - - nftDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - - const validateDripsReceiversStub = sinon.stub(validators, 'validateDripsReceivers'); + const validateSetDripsInputStub = sinon.stub(validators, 'validateSetDripsInput'); // Act await testNftDriverClient.setDrips(tokenId, tokenAddress, currentReceivers, receivers, transferToAddress, 1n); // Assert assert( - validateDripsReceiversStub.calledWithExactly( - sinon.match( - (r: DripsReceiver[]) => - r[0].userId === receivers[0].userId && - r[1].userId === receivers[1].userId && - r[2].userId === receivers[2].userId - ) - ), - 'Expected method to be called with different arguments' - ); - assert( - validateDripsReceiversStub.calledWithExactly( - sinon.match((r: DripsReceiver[]) => r[0].userId === currentReceivers[0].userId) - ), - 'Expected method to be called with different arguments' - ); - }); - - it('should throw argumentMissingError when current drips transferToAddress are missing', async () => { - // Arrange - let threw = false; - - // Act - try { - await testNftDriverClient.setDrips( - '1', - Wallet.createRandom().address, - [], - [], - undefined as unknown as string, - 0n - ); - } catch (error: any) { - // Assert - assert.equal(error.code, DripsErrorCode.MISSING_ARGUMENT); - threw = true; - } - - // Assert - assert.isTrue(threw, 'Expected type of exception was not thrown'); - }); - - it('should clear drips when new receivers is an empty list', async () => { - // Arrange - const tokenId = '1'; - const tokenAddress = Wallet.createRandom().address; - const transferToAddress = Wallet.createRandom().address; - const currentReceivers: DripsReceiverStruct[] = [ - { - userId: 3, - config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 3n, duration: 3n, start: 3n }) - } - ]; - - const estimatedGasFees = 2000000; - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - nftDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - - // Act - await testNftDriverClient.setDrips(tokenId, tokenAddress, currentReceivers, [], transferToAddress, 1n); - - // Assert - assert( - nftDriverContractStub.setDrips.calledOnceWithExactly( - tokenId, - tokenAddress, - currentReceivers, - 1n, - [], - 0, - 0, - transferToAddress, - { - gasLimit - } - ), - 'Expected method to be called with different arguments' - ); - }); - - it('should set balanceDelta to the default value of 0 when balanceDelta is not provided', async () => { - // Arrange - const tokenId = '1'; - const tokenAddress = Wallet.createRandom().address; - const transferToAddress = Wallet.createRandom().address; - - const estimatedGasFees = 2000000; - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - nftDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; - - // Act - await testNftDriverClient.setDrips( - tokenId, - tokenAddress, - [], - [], - transferToAddress, - undefined as unknown as bigint - ); - - // Assert - assert( - nftDriverContractStub.setDrips.calledOnceWithExactly( - tokenId, + validateSetDripsInputStub.calledOnceWithExactly( tokenAddress, - [], - 0, - [], - 0, - 0, + sinon.match.array.deepEquals( + currentReceivers?.map((r) => ({ + userId: r.userId.toString(), + config: Utils.DripsReceiverConfiguration.fromUint256(BigNumber.from(r.config).toBigInt()) + })) + ), + sinon.match.array.deepEquals( + receivers?.map((r) => ({ + userId: r.userId.toString(), + config: Utils.DripsReceiverConfiguration.fromUint256(BigNumber.from(r.config).toBigInt()) + })) + ), transferToAddress, - { - gasLimit - } + 1n ), 'Expected method to be called with different arguments' ); }); - it('should call the setDrips() method of the NFTDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange const tokenId = '1'; const tokenAddress = Wallet.createRandom().address; @@ -905,7 +740,7 @@ describe('NFTDriverClient', () => { config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 3n, duration: 3n, start: 3n }) } ]; - const newReceivers: DripsReceiverStruct[] = [ + const receivers: DripsReceiverStruct[] = [ { userId: 2n, config: Utils.DripsReceiverConfiguration.toUint256({ dripId: 1n, amountPerSec: 1n, duration: 1n, start: 1n }) @@ -920,40 +755,25 @@ describe('NFTDriverClient', () => { } ]; - const estimatedGasFees = 2000000; - const gasLimit = Math.ceil(estimatedGasFees + estimatedGasFees * 0.2); - - nftDriverContractStub.estimateGas = { - setDrips: () => BigNumber.from(estimatedGasFees) as any - } as any; + const balance = 1n; - sinon - .stub(internals, 'formatDripsReceivers') - .onFirstCall() - .returns(currentReceivers) - .onSecondCall() - .returns(newReceivers); + const tx = {}; + nftDriverTxFactoryStub.setDrips + .withArgs(tokenId, tokenAddress, currentReceivers, balance, receivers, 0, 0, transferToAddress) + .resolves(tx); // Act - await testNftDriverClient.setDrips(tokenId, tokenAddress, currentReceivers, newReceivers, transferToAddress, 1); + await testNftDriverClient.setDrips( + tokenId, + tokenAddress, + currentReceivers, + receivers, + transferToAddress, + balance + ); // Assert - assert( - nftDriverContractStub.setDrips.calledOnceWithExactly( - tokenId, - tokenAddress, - currentReceivers, - 1, - newReceivers, - 0, - 0, - transferToAddress, - { - gasLimit - } - ), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -993,8 +813,7 @@ describe('NFTDriverClient', () => { assert(validateSplitsReceiversStub.calledOnceWithExactly(receivers)); }); - it('should call the setSplits() method of the NFTDriver contract', async () => { - // Arrange + it('should send the expected transaction', async () => { const tokenId = '1'; const receivers: SplitsReceiverStruct[] = [ @@ -1003,20 +822,14 @@ describe('NFTDriverClient', () => { { userId: 1, weight: 1 } ]; + const tx = {}; + nftDriverTxFactoryStub.setSplits.withArgs(tokenId, receivers).resolves(tx); + // Act await testNftDriverClient.setSplits(tokenId, receivers); // Assert - assert( - nftDriverContractStub.setSplits.calledOnceWithExactly( - tokenId, - sinon - .match((r: SplitsReceiverStruct[]) => r.length === 2) - .and(sinon.match((r: SplitsReceiverStruct[]) => r[0].userId === 1)) - .and(sinon.match((r: SplitsReceiverStruct[]) => r[1].userId === 2)) - ), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); @@ -1038,20 +851,19 @@ describe('NFTDriverClient', () => { assert.isTrue(threw, 'Expected type of exception was not thrown'); }); - it('should call the emitUserMetadata() method of the NFTDriver contract', async () => { + it('should send the expected transaction', async () => { // Arrange - const tokenId = '1'; const metadata: UserMetadata[] = [{ key: 'key', value: 'value' }]; const metadataAsBytes = metadata.map((m) => internals.createFromStrings(m.key, m.value)); + const tx = {}; + nftDriverTxFactoryStub.emitUserMetadata.withArgs('1', metadataAsBytes).resolves(tx); + // Act - await testNftDriverClient.emitUserMetadata(tokenId, metadata); + await testNftDriverClient.emitUserMetadata('1', metadata); // Assert - assert( - nftDriverContractStub.emitUserMetadata.calledOnceWithExactly(tokenId, metadataAsBytes), - 'Expected method to be called with different arguments' - ); + assert(signerStub?.sendTransaction.calledOnceWithExactly(tx), 'Did not send the expected tx.'); }); }); }); diff --git a/tests/NFTDriver/NFTDriverTxFactory.tests.ts b/tests/NFTDriver/NFTDriverTxFactory.tests.ts index 09c85197..de7d61bb 100644 --- a/tests/NFTDriver/NFTDriverTxFactory.tests.ts +++ b/tests/NFTDriver/NFTDriverTxFactory.tests.ts @@ -1,38 +1,48 @@ import type { Network } from '@ethersproject/networks'; -import { JsonRpcProvider } from '@ethersproject/providers'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { assert } from 'chai'; import type { StubbedInstance } from 'ts-sinon'; import sinon, { stubObject, stubInterface } from 'ts-sinon'; -import { Wallet } from 'ethers'; +import { BigNumber, Wallet } from 'ethers'; import type { NFTDriver } from '../../contracts'; import { NFTDriver__factory } from '../../contracts'; import Utils from '../../src/utils'; import NFTDriverTxFactory from '../../src/NFTDriver/NFTDriverTxFactory'; import * as validators from '../../src/common/validators'; import type { SplitsReceiverStruct, DripsReceiverStruct, UserMetadataStruct } from '../../src/common/types'; +import { formatDripsReceivers } from '../../src/common/internals'; describe('NFTDriverTxFactory', () => { const TEST_CHAIN_ID = 5; // Goerli. let networkStub: StubbedInstance; + let signerStub: StubbedInstance; + let signerWithProviderStub: StubbedInstance; let providerStub: sinon.SinonStubbedInstance; let nftDriverContractStub: StubbedInstance; - let testNFTDriverTxFactory: NFTDriverTxFactory; + let testNftDriverTxFactory: NFTDriverTxFactory; // Acts also as the "base Arrange step". beforeEach(async () => { providerStub = sinon.createStubInstance(JsonRpcProvider); + + signerStub = sinon.createStubInstance(JsonRpcSigner); + signerStub.getAddress.resolves(Wallet.createRandom().address); + networkStub = stubObject({ chainId: TEST_CHAIN_ID } as Network); + providerStub.getNetwork.resolves(networkStub); + signerWithProviderStub = { ...signerStub, provider: providerStub }; + nftDriverContractStub = stubInterface(); sinon .stub(NFTDriver__factory, 'connect') - .withArgs(Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_ADDRESS_DRIVER, providerStub) + .withArgs(Utils.Network.configs[TEST_CHAIN_ID].CONTRACT_NFT_DRIVER, signerWithProviderStub) .returns(nftDriverContractStub); - testNFTDriverTxFactory = await NFTDriverTxFactory.create(providerStub); + testNftDriverTxFactory = await NFTDriverTxFactory.create(signerWithProviderStub); }); afterEach(() => { @@ -40,16 +50,16 @@ describe('NFTDriverTxFactory', () => { }); describe('create', async () => { - it('should validate the provider', async () => { + it('should validate the signer', async () => { // Arrange - const validateClientProviderStub = sinon.stub(validators, 'validateClientProvider'); + const validateClientSignerStub = sinon.stub(validators, 'validateClientSigner'); // Act - await NFTDriverTxFactory.create(providerStub); + await NFTDriverTxFactory.create(signerWithProviderStub); // Assert assert( - validateClientProviderStub.calledOnceWithExactly(providerStub, Utils.Network.SUPPORTED_CHAINS), + validateClientSignerStub.calledOnceWithExactly(signerWithProviderStub, Utils.Network.SUPPORTED_CHAINS), 'Expected method to be called with different arguments' ); }); @@ -59,7 +69,7 @@ describe('NFTDriverTxFactory', () => { const customDriverAddress = Wallet.createRandom().address; // Act - const client = await NFTDriverTxFactory.create(providerStub, customDriverAddress); + const client = await NFTDriverTxFactory.create(signerWithProviderStub, customDriverAddress); // Assert assert.equal(client.driverAddress, customDriverAddress); @@ -68,24 +78,42 @@ describe('NFTDriverTxFactory', () => { it('should create a fully initialized client instance', async () => { // Assert assert.equal( - testNFTDriverTxFactory.driverAddress, - Utils.Network.configs[(await providerStub.getNetwork()).chainId].CONTRACT_ADDRESS_DRIVER + testNftDriverTxFactory.driverAddress, + Utils.Network.configs[(await providerStub.getNetwork()).chainId].CONTRACT_NFT_DRIVER ); + assert.equal(testNftDriverTxFactory.signer, signerWithProviderStub); }); }); - describe('collect', () => { + describe('mint', () => { + it('should return the expected transaction', async () => { + // Arrange + const stub = sinon.stub(); + nftDriverContractStub.populateTransaction.mint = stub; + const userMetadata = [] as UserMetadataStruct[]; + const overrides = {}; + + // Act + await testNftDriverTxFactory.mint('0x1234', userMetadata, overrides); + + // Assert + assert(stub.calledOnceWithExactly('0x1234', userMetadata, overrides)); + }); + }); + + describe('safeMint', () => { it('should return the expected transaction', async () => { // Arrange const stub = sinon.stub(); nftDriverContractStub.populateTransaction.safeMint = stub; const userMetadata = [] as UserMetadataStruct[]; + const overrides = {}; // Act - await testNFTDriverTxFactory.safeMint('0x1234', userMetadata); + await testNftDriverTxFactory.safeMint('0x1234', userMetadata, overrides); // Assert - assert(stub.calledOnceWithExactly('0x1234', userMetadata)); + assert(stub.calledOnceWithExactly('0x1234', userMetadata, overrides)); }); }); @@ -94,12 +122,13 @@ describe('NFTDriverTxFactory', () => { // Arrange const stub = sinon.stub(); nftDriverContractStub.populateTransaction.collect = stub; + const overrides = {}; // Act - await testNFTDriverTxFactory.collect('0x1234', '0x5678', '0x9abc'); + await testNftDriverTxFactory.collect('0x1234', '0x5678', '0x9abc', overrides); // Assert - assert(stub.calledOnceWithExactly('0x1234', '0x5678', '0x9abc')); + assert(stub.calledOnceWithExactly('0x1234', '0x5678', '0x9abc', overrides)); }); }); @@ -108,12 +137,13 @@ describe('NFTDriverTxFactory', () => { // Arrange const stub = sinon.stub(); nftDriverContractStub.populateTransaction.give = stub; + const overrides = {}; // Act - await testNFTDriverTxFactory.give('0x1234', '0x5678', '0x9abc', '0xdef0'); + await testNftDriverTxFactory.give('0x1234', '0x5678', '0x9abc', '0xdef0', overrides); // Assert - assert(stub.calledOnceWithExactly('0x1234', '0x5678', '0x9abc', '0xdef0')); + assert(stub.calledOnceWithExactly('0x1234', '0x5678', '0x9abc', '0xdef0', overrides)); }); }); @@ -123,12 +153,13 @@ describe('NFTDriverTxFactory', () => { const stub = sinon.stub(); nftDriverContractStub.populateTransaction.setSplits = stub; const receivers = [] as SplitsReceiverStruct[]; + const overrides = {}; // Act - await testNFTDriverTxFactory.setSplits('0x1234', receivers); + await testNftDriverTxFactory.setSplits('0x1234', receivers, overrides); // Assert - assert(stub.calledOnceWithExactly('0x1234', receivers)); + assert(stub.calledOnceWithExactly('0x1234', receivers, overrides)); }); }); @@ -137,14 +168,28 @@ describe('NFTDriverTxFactory', () => { // Arrange const stub = sinon.stub(); nftDriverContractStub.populateTransaction.setDrips = stub; - const currReceivers = [] as DripsReceiverStruct[]; - const newReceivers = [] as DripsReceiverStruct[]; + const currReceivers = [{ userId: 2 }, { userId: 1 }] as DripsReceiverStruct[]; + const newReceivers = [{ userId: 2 }, { userId: 1 }] as DripsReceiverStruct[]; + + nftDriverContractStub.estimateGas.setDrips = sinon.stub().resolves(BigNumber.from(100)); // Act - await testNFTDriverTxFactory.setDrips('0x1234', '0xdef0', currReceivers, '0x5678', newReceivers, '0x9abc'); + await testNftDriverTxFactory.setDrips('1', '0x1234', currReceivers, '0x5678', newReceivers, 0, 0, '0x9abc'); // Assert - assert(stub.calledOnceWithExactly('0x1234', '0xdef0', currReceivers, '0x5678', newReceivers, 0, 0, '0x9abc')); + assert( + stub.calledOnceWithExactly( + '1', + '0x1234', + formatDripsReceivers(currReceivers), + '0x5678', + formatDripsReceivers(newReceivers), + 0, + 0, + '0x9abc', + { gasLimit: 120 } + ) + ); }); }); @@ -154,12 +199,13 @@ describe('NFTDriverTxFactory', () => { const stub = sinon.stub(); nftDriverContractStub.populateTransaction.emitUserMetadata = stub; const userMetadata = [] as UserMetadataStruct[]; + const overrides = {}; // Act - await testNFTDriverTxFactory.emitUserMetadata('0xdef0', userMetadata); + await testNftDriverTxFactory.emitUserMetadata('0xdef0', userMetadata, overrides); // Assert - assert(stub.calledOnceWithExactly('0xdef0', userMetadata)); + assert(stub.calledOnceWithExactly('0xdef0', userMetadata, overrides)); }); }); }); From 9b87cab42544da9932bb00d77b63ffc34754b1de Mon Sep 17 00:00:00 2001 From: Ioannis Tourkogiorgis Date: Tue, 14 Mar 2023 18:14:34 +0100 Subject: [PATCH 6/6] refactor(presets): reuse tx factories instead of the generated API --- src/AddressDriver/AddressDriverPresets.ts | 133 ++++----- src/DripsHub/DripsHubTxFactory.ts | 2 + src/NFTDriver/NFTDriverPresets.ts | 142 ++++------ src/common/types.ts | 4 +- .../AddressDriverPresets.tests.ts | 247 +++++++++-------- tests/NFTDriver/NFTDriverClient.tests.ts | 2 +- tests/NFTDriver/NFTDriverPresets.tests.ts | 254 ++++++++++-------- 7 files changed, 406 insertions(+), 378 deletions(-) diff --git a/src/AddressDriver/AddressDriverPresets.ts b/src/AddressDriver/AddressDriverPresets.ts index b18f4aae..dfc4307c 100644 --- a/src/AddressDriver/AddressDriverPresets.ts +++ b/src/AddressDriver/AddressDriverPresets.ts @@ -1,6 +1,6 @@ -import type { CallStruct } from 'contracts/Caller'; -import type { BigNumberish } from 'ethers'; +import type { BigNumberish, PopulatedTransaction, Signer } from 'ethers'; import { BigNumber } from 'ethers'; +import DripsHubTxFactory from '../DripsHub/DripsHubTxFactory'; import { validateCollectInput, validateEmitUserMetadataInput, @@ -9,20 +9,15 @@ import { validateSplitInput, validateSqueezeDripsInput } from '../common/validators'; -import { - createFromStrings, - formatDripsReceivers, - formatSplitReceivers, - isNullOrUndefined, - nameOf -} from '../common/internals'; +import { createFromStrings, isNullOrUndefined, nameOf } from '../common/internals'; import Utils from '../utils'; import type { DripsReceiverStruct, Preset, SplitsReceiverStruct, SqueezeArgs, UserMetadata } from '../common/types'; import { DripsErrors } from '../common/DripsError'; -import { AddressDriver__factory, DripsHub__factory } from '../../contracts/factories'; +import AddressDriverTxFactory from './AddressDriverTxFactory'; export namespace AddressDriverPresets { export type NewStreamFlowPayload = { + signer: Signer; driverAddress: string; tokenAddress: string; currentReceivers: DripsReceiverStruct[]; @@ -33,6 +28,7 @@ export namespace AddressDriverPresets { }; export type CollectFlowPayload = { + signer: Signer; driverAddress: string; dripsHubAddress: string; userId: string; @@ -85,7 +81,7 @@ export namespace AddressDriverPresets { * @throws {@link DripsErrors.dripsReceiverError} if any of the `payload.currentReceivers` or the `payload.newReceivers` is not valid. * @throws {@link DripsErrors.dripsReceiverConfigError} if any of the receivers' configuration is not valid. */ - public static createNewStreamFlow(payload: NewStreamFlowPayload): Preset { + public static async createNewStreamFlow(payload: NewStreamFlowPayload): Promise { if (isNullOrUndefined(payload)) { throw DripsErrors.argumentMissingError( `Could not create stream flow: '${nameOf({ payload })}' is missing.`, @@ -94,6 +90,7 @@ export namespace AddressDriverPresets { } const { + signer, userMetadata, tokenAddress, driverAddress, @@ -103,6 +100,10 @@ export namespace AddressDriverPresets { transferToAddress } = payload; + if (!signer?.provider) { + throw DripsErrors.argumentError(`Could not create collect flow: signer is not connected to a provider.`); + } + validateSetDripsInput( tokenAddress, currentReceivers?.map((r) => ({ @@ -118,29 +119,23 @@ export namespace AddressDriverPresets { ); validateEmitUserMetadataInput(userMetadata); - const setDrips: CallStruct = { - value: 0, - to: driverAddress, - data: AddressDriver__factory.createInterface().encodeFunctionData('setDrips', [ - tokenAddress, - formatDripsReceivers(currentReceivers), - balanceDelta, - formatDripsReceivers(newReceivers), - 0, - 0, - transferToAddress - ]) - }; + const addressDriverTxFactory = await AddressDriverTxFactory.create(signer, driverAddress); + + const setDripsTx = await addressDriverTxFactory.setDrips( + tokenAddress, + currentReceivers, + balanceDelta, + newReceivers, + 0, + 0, + transferToAddress + ); const userMetadataAsBytes = userMetadata.map((m) => createFromStrings(m.key, m.value)); - const emitUserMetadata: CallStruct = { - value: 0, - to: driverAddress, - data: AddressDriver__factory.createInterface().encodeFunctionData('emitUserMetadata', [userMetadataAsBytes]) - }; + const emitUserMetadataTx = await addressDriverTxFactory.emitUserMetadata(userMetadataAsBytes); - return [setDrips, emitUserMetadata]; + return [setDripsTx, emitUserMetadataTx]; } /** @@ -160,11 +155,11 @@ export namespace AddressDriverPresets { * @throws {@link DripsErrors.argumentError} if `payload.maxCycles` or `payload.currentReceivers` is not valid. * @throws {@link DripsErrors.splitsReceiverError} if any of the `payload.currentReceivers` is not valid. */ - public static createCollectFlow( + public static async createCollectFlow( payload: CollectFlowPayload, skipReceive: boolean = false, skipSplit: boolean = false - ): Preset { + ): Promise { if (isNullOrUndefined(payload)) { throw DripsErrors.argumentMissingError( `Could not create collect flow: '${nameOf({ payload })}' is missing.`, @@ -173,6 +168,7 @@ export namespace AddressDriverPresets { } const { + signer, driverAddress, dripsHubAddress, userId, @@ -183,64 +179,51 @@ export namespace AddressDriverPresets { squeezeArgs } = payload; - const flow: CallStruct[] = []; + if (!signer?.provider) { + throw DripsErrors.argumentError(`Could not create collect flow: signer is not connected to a provider.`); + } - squeezeArgs?.forEach((args) => { + const flow: PopulatedTransaction[] = []; + + const dripsHubTxFactory = await DripsHubTxFactory.create(signer.provider, dripsHubAddress); + + squeezeArgs?.forEach(async (args) => { validateSqueezeDripsInput(args.userId, args.tokenAddress, args.senderId, args.historyHash, args.dripsHistory); - const squeeze: CallStruct = { - value: 0, - to: dripsHubAddress, - data: DripsHub__factory.createInterface().encodeFunctionData('squeezeDrips', [ - userId, - tokenAddress, - args.senderId, - args.historyHash, - args.dripsHistory - ]) - }; - - flow.push(squeeze); + const squeezeTx = await dripsHubTxFactory.squeezeDrips( + userId, + tokenAddress, + args.senderId, + args.historyHash, + args.dripsHistory + ); + + flow.push(squeezeTx); }); if (!skipReceive) { validateReceiveDripsInput(userId, tokenAddress, maxCycles); - const receive: CallStruct = { - value: 0, - to: dripsHubAddress, - data: DripsHub__factory.createInterface().encodeFunctionData('receiveDrips', [ - userId, - tokenAddress, - maxCycles - ]) - }; - - flow.push(receive); + + const receiveTx = await dripsHubTxFactory.receiveDrips(userId, tokenAddress, maxCycles); + + flow.push(receiveTx); } if (!skipSplit) { validateSplitInput(userId, tokenAddress, currentReceivers); - const split: CallStruct = { - value: 0, - to: dripsHubAddress, - data: DripsHub__factory.createInterface().encodeFunctionData('split', [ - userId, - tokenAddress, - formatSplitReceivers(currentReceivers) - ]) - }; - - flow.push(split); + + const splitTx = await dripsHubTxFactory.split(userId, tokenAddress, currentReceivers); + + flow.push(splitTx); } validateCollectInput(tokenAddress, transferToAddress); - const collect: CallStruct = { - value: 0, - to: driverAddress, - data: AddressDriver__factory.createInterface().encodeFunctionData('collect', [tokenAddress, transferToAddress]) - }; - flow.push(collect); + const addressDriverTxFactory = await AddressDriverTxFactory.create(signer, driverAddress); + + const collectTx = await addressDriverTxFactory.collect(tokenAddress, transferToAddress); + + flow.push(collectTx); return flow; } diff --git a/src/DripsHub/DripsHubTxFactory.ts b/src/DripsHub/DripsHubTxFactory.ts index eadd78e1..f66c3229 100644 --- a/src/DripsHub/DripsHubTxFactory.ts +++ b/src/DripsHub/DripsHubTxFactory.ts @@ -39,6 +39,7 @@ export default class DripsHubTxFactory implements IDripsHubTxFactory { ): Promise { return this.#driver.populateTransaction.receiveDrips(userId, erc20, maxCycles); } + squeezeDrips( userId: PromiseOrValue, erc20: PromiseOrValue, @@ -48,6 +49,7 @@ export default class DripsHubTxFactory implements IDripsHubTxFactory { ): Promise { return this.#driver.populateTransaction.squeezeDrips(userId, erc20, senderId, historyHash, dripsHistory); } + split( userId: PromiseOrValue, erc20: PromiseOrValue, diff --git a/src/NFTDriver/NFTDriverPresets.ts b/src/NFTDriver/NFTDriverPresets.ts index 618b3104..d664403f 100644 --- a/src/NFTDriver/NFTDriverPresets.ts +++ b/src/NFTDriver/NFTDriverPresets.ts @@ -1,6 +1,6 @@ -import type { CallStruct } from 'contracts/Caller'; -import type { BigNumberish } from 'ethers'; +import type { BigNumberish, PopulatedTransaction, Signer } from 'ethers'; import { BigNumber } from 'ethers'; +import DripsHubTxFactory from '../DripsHub/DripsHubTxFactory'; import { validateCollectInput, validateEmitUserMetadataInput, @@ -9,21 +9,16 @@ import { validateSplitInput, validateSqueezeDripsInput } from '../common/validators'; -import { - createFromStrings, - formatDripsReceivers, - formatSplitReceivers, - isNullOrUndefined, - nameOf -} from '../common/internals'; +import { createFromStrings, isNullOrUndefined, nameOf } from '../common/internals'; import Utils from '../utils'; import type { DripsReceiverStruct, Preset, SplitsReceiverStruct, SqueezeArgs, UserMetadata } from '../common/types'; import { DripsErrors } from '../common/DripsError'; -import { NFTDriver__factory, DripsHub__factory } from '../../contracts/factories'; +import NFTDriverTxFactory from './NFTDriverTxFactory'; export namespace NFTDriverPresets { export type NewStreamFlowPayload = { tokenId: string; + signer: Signer; driverAddress: string; tokenAddress: string; currentReceivers: DripsReceiverStruct[]; @@ -35,6 +30,7 @@ export namespace NFTDriverPresets { export type CollectFlowPayload = { tokenId: string; + signer: Signer; driverAddress: string; dripsHubAddress: string; userId: string; @@ -87,7 +83,7 @@ export namespace NFTDriverPresets { * @throws {@link DripsErrors.dripsReceiverError} if any of the `payload.currentReceivers` or the `payload.newReceivers` is not valid. * @throws {@link DripsErrors.dripsReceiverConfigError} if any of the receivers' configuration is not valid. */ - public static createNewStreamFlow(payload: NewStreamFlowPayload): Preset { + public static async createNewStreamFlow(payload: NewStreamFlowPayload): Promise { if (isNullOrUndefined(payload)) { throw DripsErrors.argumentMissingError( `Could not create stream flow: '${nameOf({ payload })}' is missing.`, @@ -96,6 +92,7 @@ export namespace NFTDriverPresets { } const { + signer, tokenId, userMetadata, tokenAddress, @@ -114,6 +111,10 @@ export namespace NFTDriverPresets { ); } + if (!signer?.provider) { + throw DripsErrors.argumentError(`Could not create collect flow: signer is not connected to a provider.`); + } + validateSetDripsInput( tokenAddress, currentReceivers?.map((r) => ({ @@ -129,33 +130,24 @@ export namespace NFTDriverPresets { ); validateEmitUserMetadataInput(userMetadata); - const setDrips: CallStruct = { - value: 0, - to: driverAddress, - data: NFTDriver__factory.createInterface().encodeFunctionData('setDrips', [ - tokenId, - tokenAddress, - formatDripsReceivers(currentReceivers), - balanceDelta, - formatDripsReceivers(newReceivers), - 0, - 0, - transferToAddress - ]) - }; + const nftDriverTxFactory = await NFTDriverTxFactory.create(signer, driverAddress); + + const setDripsTx = await nftDriverTxFactory.setDrips( + tokenId, + tokenAddress, + currentReceivers, + balanceDelta, + newReceivers, + 0, + 0, + transferToAddress + ); const userMetadataAsBytes = userMetadata.map((m) => createFromStrings(m.key, m.value)); - const emitUserMetadata: CallStruct = { - value: 0, - to: driverAddress, - data: NFTDriver__factory.createInterface().encodeFunctionData('emitUserMetadata', [ - tokenId, - userMetadataAsBytes - ]) - }; + const emitUserMetadataTx = await nftDriverTxFactory.emitUserMetadata(tokenId, userMetadataAsBytes); - return [setDrips, emitUserMetadata]; + return [setDripsTx, emitUserMetadataTx]; } /** @@ -175,11 +167,11 @@ export namespace NFTDriverPresets { * @throws {@link DripsErrors.argumentError} if `payload.maxCycles` or `payload.currentReceivers` is not valid. * @throws {@link DripsErrors.splitsReceiverError} if any of the `payload.currentReceivers` is not valid. */ - public static createCollectFlow( + public static async createCollectFlow( payload: CollectFlowPayload, skipReceive: boolean = false, skipSplit: boolean = false - ): Preset { + ): Promise { if (isNullOrUndefined(payload)) { throw DripsErrors.argumentMissingError( `Could not create collect flow: '${nameOf({ payload })}' is missing.`, @@ -189,6 +181,7 @@ export namespace NFTDriverPresets { const { tokenId, + signer, driverAddress, dripsHubAddress, userId, @@ -207,68 +200,51 @@ export namespace NFTDriverPresets { ); } - const flow: CallStruct[] = []; + if (!signer?.provider) { + throw DripsErrors.argumentError(`Could not create collect flow: signer is not connected to a provider.`); + } - squeezeArgs?.forEach((args) => { + const flow: PopulatedTransaction[] = []; + + const dripsHubTxFactory = await DripsHubTxFactory.create(signer.provider, dripsHubAddress); + + squeezeArgs?.forEach(async (args) => { validateSqueezeDripsInput(args.userId, args.tokenAddress, args.senderId, args.historyHash, args.dripsHistory); - const squeeze: CallStruct = { - value: 0, - to: dripsHubAddress, - data: DripsHub__factory.createInterface().encodeFunctionData('squeezeDrips', [ - userId, - tokenAddress, - args.senderId, - args.historyHash, - args.dripsHistory - ]) - }; - - flow.push(squeeze); + const squeezeTx = await dripsHubTxFactory.squeezeDrips( + userId, + tokenAddress, + args.senderId, + args.historyHash, + args.dripsHistory + ); + + flow.push(squeezeTx); }); if (!skipReceive) { validateReceiveDripsInput(userId, tokenAddress, maxCycles); - const receive: CallStruct = { - value: 0, - to: dripsHubAddress, - data: DripsHub__factory.createInterface().encodeFunctionData('receiveDrips', [ - userId, - tokenAddress, - maxCycles - ]) - }; - - flow.push(receive); + + const receiveTx = await dripsHubTxFactory.receiveDrips(userId, tokenAddress, maxCycles); + + flow.push(receiveTx); } if (!skipSplit) { validateSplitInput(userId, tokenAddress, currentReceivers); - const split: CallStruct = { - value: 0, - to: dripsHubAddress, - data: DripsHub__factory.createInterface().encodeFunctionData('split', [ - userId, - tokenAddress, - formatSplitReceivers(currentReceivers) - ]) - }; - - flow.push(split); + + const splitTx = await dripsHubTxFactory.split(userId, tokenAddress, currentReceivers); + + flow.push(splitTx); } validateCollectInput(tokenAddress, transferToAddress); - const collect: CallStruct = { - value: 0, - to: driverAddress, - data: NFTDriver__factory.createInterface().encodeFunctionData('collect', [ - tokenId, - tokenAddress, - transferToAddress - ]) - }; - flow.push(collect); + const nftDriverTxFactory = await NFTDriverTxFactory.create(signer, driverAddress); + + const collectTx = await nftDriverTxFactory.collect(tokenId, tokenAddress, transferToAddress); + + flow.push(collectTx); return flow; } diff --git a/src/common/types.ts b/src/common/types.ts index 19fcd145..ea368c16 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,4 +1,4 @@ -import type { CallStruct } from '../../contracts/Caller'; +import type { PopulatedTransaction } from 'ethers'; import type { DripsHistoryStruct } from '../../contracts/DripsHub'; export { @@ -54,7 +54,7 @@ export type CycleInfo = { export type DripsReceiver = { userId: string; config: DripsReceiverConfig }; -export type Preset = CallStruct[]; +export type Preset = PopulatedTransaction[]; export type UserMetadata = { key: string; diff --git a/tests/AddressDriver/AddressDriverPresets.tests.ts b/tests/AddressDriver/AddressDriverPresets.tests.ts index e01aec0f..21dd34e0 100644 --- a/tests/AddressDriver/AddressDriverPresets.tests.ts +++ b/tests/AddressDriver/AddressDriverPresets.tests.ts @@ -1,27 +1,46 @@ import { assert } from 'chai'; +import type { BytesLike, PopulatedTransaction } from 'ethers'; import { BigNumber, Wallet } from 'ethers'; import type { StubbedInstance } from 'ts-sinon'; -import sinon, { stubInterface } from 'ts-sinon'; -import { AddressDriver__factory, DripsHub__factory } from '../../contracts'; -import type { AddressDriverInterface } from '../../contracts/AddressDriver'; -import type { DripsHubInterface } from '../../contracts/DripsHub'; +import sinon, { stubObject, stubInterface } from 'ts-sinon'; +import type { Network } from '@ethersproject/networks'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { AddressDriverPresets } from '../../src/AddressDriver/AddressDriverPresets'; import { DripsErrorCode } from '../../src/common/DripsError'; -import { formatDripsReceivers, formatSplitReceivers, keyFromString, valueFromString } from '../../src/common/internals'; -import type { UserMetadataStruct } from '../../src/common/types'; +import { keyFromString, valueFromString } from '../../src/common/internals'; import * as validators from '../../src/common/validators'; import Utils from '../../src/utils'; +import AddressDriverTxFactory from '../../src/AddressDriver/AddressDriverTxFactory'; +import DripsHubTxFactory from '../../src/DripsHub/DripsHubTxFactory'; describe('AddressDriverPresets', () => { - let dripsHubInterfaceStub: StubbedInstance; - let addressDriverInterfaceStub: StubbedInstance; + const TEST_CHAIN_ID = 5; // Goerli. + + let networkStub: StubbedInstance; + let signerStub: StubbedInstance; + let signerWithProviderStub: StubbedInstance; + let providerStub: sinon.SinonStubbedInstance; + let dripsHubTxFactoryStub: StubbedInstance; + let addressDriverFactoryStub: StubbedInstance; beforeEach(async () => { - dripsHubInterfaceStub = stubInterface(); - addressDriverInterfaceStub = stubInterface(); + dripsHubTxFactoryStub = stubInterface(); + addressDriverFactoryStub = stubInterface(); + + providerStub = sinon.createStubInstance(JsonRpcProvider); + + signerStub = sinon.createStubInstance(JsonRpcSigner); + signerStub.getAddress.resolves(Wallet.createRandom().address); + + networkStub = stubObject({ chainId: TEST_CHAIN_ID } as Network); - sinon.stub(AddressDriver__factory, 'createInterface').returns(addressDriverInterfaceStub); - sinon.stub(DripsHub__factory, 'createInterface').returns(dripsHubInterfaceStub); + providerStub.getNetwork.resolves(networkStub); + + signerWithProviderStub = { ...signerStub, provider: providerStub }; + signerStub.connect.withArgs(providerStub).returns(signerWithProviderStub); + + sinon.stub(AddressDriverTxFactory, 'create').resolves(addressDriverFactoryStub); + sinon.stub(DripsHubTxFactory, 'create').resolves(dripsHubTxFactoryStub); }); afterEach(() => { @@ -35,7 +54,7 @@ describe('AddressDriverPresets', () => { try { // Act - AddressDriverPresets.Presets.createNewStreamFlow( + await AddressDriverPresets.Presets.createNewStreamFlow( undefined as unknown as AddressDriverPresets.NewStreamFlowPayload ); } catch (error: any) { @@ -48,11 +67,29 @@ describe('AddressDriverPresets', () => { assert.isTrue(threw, 'Expected type of exception was not thrown'); }); + it("it should throw an argumentError when signer's provider is missing", async () => { + // Arrange + let threw = false; + + try { + // Act + await AddressDriverPresets.Presets.createNewStreamFlow({} as AddressDriverPresets.NewStreamFlowPayload); + } catch (error: any) { + // Assert + assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); + threw = true; + } + + // Assert + assert.isTrue(threw, 'Expected type of exception was not thrown'); + }); + it('should validate the setDrips input', async () => { // Arrange const validateSetDripsInputStub = sinon.stub(validators, 'validateSetDripsInput'); const payload: AddressDriverPresets.NewStreamFlowPayload = { + signer: signerWithProviderStub, userMetadata: [], balanceDelta: 1, currentReceivers: [ @@ -83,7 +120,7 @@ describe('AddressDriverPresets', () => { }; // Act - AddressDriverPresets.Presets.createNewStreamFlow(payload); + await AddressDriverPresets.Presets.createNewStreamFlow(payload); // Assert assert( @@ -113,6 +150,7 @@ describe('AddressDriverPresets', () => { const validateEmitUserMetadataInputStub = sinon.stub(validators, 'validateEmitUserMetadataInput'); const payload: AddressDriverPresets.NewStreamFlowPayload = { + signer: signerWithProviderStub, userMetadata: [], balanceDelta: 1, currentReceivers: [ @@ -143,7 +181,7 @@ describe('AddressDriverPresets', () => { }; // Act - AddressDriverPresets.Presets.createNewStreamFlow(payload); + await AddressDriverPresets.Presets.createNewStreamFlow(payload); // Assert assert( @@ -152,12 +190,13 @@ describe('AddressDriverPresets', () => { ); }); - it('should return the expected preset', () => { + it('should return the expected preset', async () => { // Arrange sinon.stub(validators, 'validateSetDripsInput'); sinon.stub(validators, 'validateEmitUserMetadataInput'); const payload: AddressDriverPresets.NewStreamFlowPayload = { + signer: signerWithProviderStub, userMetadata: [{ key: 'key', value: 'value' }], balanceDelta: 1, currentReceivers: [ @@ -187,35 +226,35 @@ describe('AddressDriverPresets', () => { transferToAddress: Wallet.createRandom().address }; - addressDriverInterfaceStub.encodeFunctionData + addressDriverFactoryStub.setDrips .withArgs( - sinon.match((s: string) => s === 'setDrips'), - sinon.match.array.deepEquals([ - payload.tokenAddress, - formatDripsReceivers(payload.currentReceivers), - payload.balanceDelta, - formatDripsReceivers(payload.newReceivers), - 0, - 0, - payload.transferToAddress - ]) + payload.tokenAddress, + payload.currentReceivers, + payload.balanceDelta, + payload.newReceivers, + 0, + 0, + payload.transferToAddress ) - .returns('setDrips'); + .resolves({ data: 'setDrips' } as PopulatedTransaction); - addressDriverInterfaceStub.encodeFunctionData + addressDriverFactoryStub.emitUserMetadata .withArgs( - sinon.match((s: string) => s === 'emitUserMetadata'), sinon.match( - (values: UserMetadataStruct[][]) => - values[0][0].key === keyFromString(payload.userMetadata[0].key) && - values[0][0].value === valueFromString(payload.userMetadata[0].value) + ( + userMetadataAsBytes: { + key: BytesLike; + value: BytesLike; + }[] + ) => + userMetadataAsBytes[0].key === keyFromString(payload.userMetadata[0].key) && + userMetadataAsBytes[0].value === valueFromString(payload.userMetadata[0].value) ) ) - - .returns('emitUserMetadata'); + .resolves({ data: 'emitUserMetadata' } as PopulatedTransaction); // Act - const preset = AddressDriverPresets.Presets.createNewStreamFlow(payload); + const preset = await AddressDriverPresets.Presets.createNewStreamFlow(payload); // Assert assert.equal(preset.length, 2); @@ -231,7 +270,9 @@ describe('AddressDriverPresets', () => { try { // Act - AddressDriverPresets.Presets.createCollectFlow(undefined as unknown as AddressDriverPresets.CollectFlowPayload); + await AddressDriverPresets.Presets.createCollectFlow( + undefined as unknown as AddressDriverPresets.CollectFlowPayload + ); } catch (error: any) { // Assert assert.equal(error.code, DripsErrorCode.MISSING_ARGUMENT); @@ -242,11 +283,29 @@ describe('AddressDriverPresets', () => { assert.isTrue(threw, 'Expected type of exception was not thrown'); }); + it("it should throw an argumentError when signer's provider is missing", async () => { + // Arrange + let threw = false; + + try { + // Act + await AddressDriverPresets.Presets.createCollectFlow({} as AddressDriverPresets.CollectFlowPayload); + } catch (error: any) { + // Assert + assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); + threw = true; + } + + // Assert + assert.isTrue(threw, 'Expected type of exception was not thrown'); + }); + it('should validate the squeezeDrips input', async () => { // Arrange const validateSqueezeDripsInputStub = sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -271,7 +330,7 @@ describe('AddressDriverPresets', () => { }; // Act - AddressDriverPresets.Presets.createCollectFlow(payload); + await AddressDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -291,6 +350,7 @@ describe('AddressDriverPresets', () => { const validateReceiveDripsInputStub = sinon.stub(validators, 'validateReceiveDripsInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -306,7 +366,7 @@ describe('AddressDriverPresets', () => { }; // Act - AddressDriverPresets.Presets.createCollectFlow(payload); + await AddressDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -320,6 +380,7 @@ describe('AddressDriverPresets', () => { const validateSplitInputStub = sinon.stub(validators, 'validateSplitInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -335,7 +396,7 @@ describe('AddressDriverPresets', () => { }; // Act - AddressDriverPresets.Presets.createCollectFlow(payload); + await AddressDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -349,6 +410,7 @@ describe('AddressDriverPresets', () => { const validateCollectInputStub = sinon.stub(validators, 'validateCollectInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -364,7 +426,7 @@ describe('AddressDriverPresets', () => { }; // Act - AddressDriverPresets.Presets.createCollectFlow(payload); + await AddressDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -373,7 +435,7 @@ describe('AddressDriverPresets', () => { ); }); - it('should return the expected preset', () => { + it('should return the expected preset', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -381,6 +443,7 @@ describe('AddressDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -415,40 +478,22 @@ describe('AddressDriverPresets', () => { ] }; - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'squeezeDrips'), - sinon.match.array - ) - .returns('squeezeDrips'); + dripsHubTxFactoryStub.squeezeDrips.resolves({ data: 'squeezeDrips' } as PopulatedTransaction); - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'receiveDrips'), - sinon.match.array.deepEquals([payload.userId, payload.tokenAddress, payload.maxCycles]) - ) - .returns('receiveDrips'); + dripsHubTxFactoryStub.receiveDrips + .withArgs(payload.userId, payload.tokenAddress, payload.maxCycles) + .resolves({ data: 'receiveDrips' } as PopulatedTransaction); - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'split'), - sinon.match.array.deepEquals([ - payload.userId, - payload.tokenAddress, - formatSplitReceivers(payload.currentReceivers) - ]) - ) - .returns('split'); + dripsHubTxFactoryStub.split + .withArgs(payload.userId, payload.tokenAddress, payload.currentReceivers) + .resolves({ data: 'split' } as PopulatedTransaction); - addressDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + addressDriverFactoryStub.collect + .withArgs(payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = AddressDriverPresets.Presets.createCollectFlow(payload); + const preset = await AddressDriverPresets.Presets.createCollectFlow(payload); // Assert assert.equal(preset.length, 5); @@ -459,7 +504,7 @@ describe('AddressDriverPresets', () => { assert.equal(preset[4].data, 'collect'); }); - it('should return the expected preset when skip receiveDrips is true', () => { + it('should return the expected preset when skip receiveDrips is true', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -467,6 +512,7 @@ describe('AddressDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -481,22 +527,16 @@ describe('AddressDriverPresets', () => { ] }; - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'split'), - sinon.match.array.deepEquals([payload.userId, payload.tokenAddress, payload.currentReceivers]) - ) - .returns('split'); + dripsHubTxFactoryStub.split + .withArgs(payload.userId, payload.tokenAddress, payload.currentReceivers) + .resolves({ data: 'split' } as PopulatedTransaction); - addressDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + addressDriverFactoryStub.collect + .withArgs(payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = AddressDriverPresets.Presets.createCollectFlow(payload, true, false); + const preset = await AddressDriverPresets.Presets.createCollectFlow(payload, true, false); // Assert assert.equal(preset.length, 2); @@ -504,7 +544,7 @@ describe('AddressDriverPresets', () => { assert.equal(preset[1].data, 'collect'); }); - it('should return the expected preset when skip split is true', () => { + it('should return the expected preset when skip split is true', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -512,6 +552,7 @@ describe('AddressDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -526,22 +567,16 @@ describe('AddressDriverPresets', () => { ] }; - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'receiveDrips'), - sinon.match.array.deepEquals([payload.userId, payload.tokenAddress, payload.maxCycles]) - ) - .returns('receiveDrips'); + dripsHubTxFactoryStub.receiveDrips + .withArgs(payload.userId, payload.tokenAddress, payload.maxCycles) + .resolves({ data: 'receiveDrips' } as PopulatedTransaction); - addressDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + addressDriverFactoryStub.collect + .withArgs(payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = AddressDriverPresets.Presets.createCollectFlow(payload, false, true); + const preset = await AddressDriverPresets.Presets.createCollectFlow(payload, false, true); // Assert assert.equal(preset.length, 2); @@ -549,7 +584,7 @@ describe('AddressDriverPresets', () => { assert.equal(preset[1].data, 'collect'); }); - it('should return the expected preset when skip receiveDrips and split are true', () => { + it('should return the expected preset when skip receiveDrips and split are true', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -557,6 +592,7 @@ describe('AddressDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: AddressDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, userId: '1', maxCycles: 1, tokenAddress: Wallet.createRandom().address, @@ -571,15 +607,12 @@ describe('AddressDriverPresets', () => { ] }; - addressDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + addressDriverFactoryStub.collect + .withArgs(payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = AddressDriverPresets.Presets.createCollectFlow(payload, true, true); + const preset = await AddressDriverPresets.Presets.createCollectFlow(payload, true, true); // Assert assert.equal(preset.length, 1); diff --git a/tests/NFTDriver/NFTDriverClient.tests.ts b/tests/NFTDriver/NFTDriverClient.tests.ts index e81b4327..73c88080 100644 --- a/tests/NFTDriver/NFTDriverClient.tests.ts +++ b/tests/NFTDriver/NFTDriverClient.tests.ts @@ -5,7 +5,6 @@ import sinon, { stubInterface, stubObject } from 'ts-sinon'; import type { ContractReceipt, ContractTransaction } from 'ethers'; import { ethers, BigNumber, constants, Wallet } from 'ethers'; import { assert } from 'chai'; -import { DripsErrorCode } from 'radicle-drips'; import NFTDriverClient from '../../src/NFTDriver/NFTDriverClient'; import Utils from '../../src/utils'; import * as validators from '../../src/common/validators'; @@ -14,6 +13,7 @@ import type { IERC20, NFTDriver } from '../../contracts'; import { NFTDriver__factory, IERC20__factory } from '../../contracts'; import type { DripsReceiverStruct, SplitsReceiverStruct, UserMetadata } from '../../src/common/types'; import * as internals from '../../src/common/internals'; +import { DripsErrorCode } from '../../src/common/DripsError'; describe('NFTDriverClient', () => { const TEST_CHAIN_ID = 5; // Goerli. diff --git a/tests/NFTDriver/NFTDriverPresets.tests.ts b/tests/NFTDriver/NFTDriverPresets.tests.ts index a7b5d182..75babcf3 100644 --- a/tests/NFTDriver/NFTDriverPresets.tests.ts +++ b/tests/NFTDriver/NFTDriverPresets.tests.ts @@ -1,26 +1,46 @@ import { assert } from 'chai'; +import type { BytesLike, PopulatedTransaction } from 'ethers'; import { BigNumber, Wallet } from 'ethers'; import type { StubbedInstance } from 'ts-sinon'; -import sinon, { stubInterface } from 'ts-sinon'; -import { NFTDriver__factory, DripsHub__factory } from '../../contracts'; -import type { NFTDriverInterface } from '../../contracts/NFTDriver'; -import type { DripsHubInterface } from '../../contracts/DripsHub'; +import sinon, { stubObject, stubInterface } from 'ts-sinon'; +import type { Network } from '@ethersproject/networks'; +import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'; import { NFTDriverPresets } from '../../src/NFTDriver/NFTDriverPresets'; import { DripsErrorCode } from '../../src/common/DripsError'; -import { formatDripsReceivers, formatSplitReceivers, keyFromString, valueFromString } from '../../src/common/internals'; +import { keyFromString, valueFromString } from '../../src/common/internals'; import * as validators from '../../src/common/validators'; import Utils from '../../src/utils'; +import NFTDriverTxFactory from '../../src/NFTDriver/NFTDriverTxFactory'; +import DripsHubTxFactory from '../../src/DripsHub/DripsHubTxFactory'; describe('NFTDriverPresets', () => { - let dripsHubInterfaceStub: StubbedInstance; - let nftDriverInterfaceStub: StubbedInstance; + const TEST_CHAIN_ID = 5; // Goerli. + + let networkStub: StubbedInstance; + let signerStub: StubbedInstance; + let signerWithProviderStub: StubbedInstance; + let providerStub: sinon.SinonStubbedInstance; + let dripsHubTxFactoryStub: StubbedInstance; + let nftDriverFactoryStub: StubbedInstance; beforeEach(async () => { - dripsHubInterfaceStub = stubInterface(); - nftDriverInterfaceStub = stubInterface(); + dripsHubTxFactoryStub = stubInterface(); + nftDriverFactoryStub = stubInterface(); + + providerStub = sinon.createStubInstance(JsonRpcProvider); + + signerStub = sinon.createStubInstance(JsonRpcSigner); + signerStub.getAddress.resolves(Wallet.createRandom().address); + + networkStub = stubObject({ chainId: TEST_CHAIN_ID } as Network); - sinon.stub(NFTDriver__factory, 'createInterface').returns(nftDriverInterfaceStub); - sinon.stub(DripsHub__factory, 'createInterface').returns(dripsHubInterfaceStub); + providerStub.getNetwork.resolves(networkStub); + + signerWithProviderStub = { ...signerStub, provider: providerStub }; + signerStub.connect.withArgs(providerStub).returns(signerWithProviderStub); + + sinon.stub(NFTDriverTxFactory, 'create').resolves(nftDriverFactoryStub); + sinon.stub(DripsHubTxFactory, 'create').resolves(dripsHubTxFactoryStub); }); afterEach(() => { @@ -34,7 +54,9 @@ describe('NFTDriverPresets', () => { try { // Act - NFTDriverPresets.Presets.createNewStreamFlow(undefined as unknown as NFTDriverPresets.NewStreamFlowPayload); + await NFTDriverPresets.Presets.createNewStreamFlow( + undefined as unknown as NFTDriverPresets.NewStreamFlowPayload + ); } catch (error: any) { // Assert assert.equal(error.code, DripsErrorCode.MISSING_ARGUMENT); @@ -45,13 +67,30 @@ describe('NFTDriverPresets', () => { assert.isTrue(threw, 'Expected type of exception was not thrown'); }); + it("it should throw an argumentError when signer's provider is missing", async () => { + // Arrange + let threw = false; + + try { + // Act + await NFTDriverPresets.Presets.createNewStreamFlow({ tokenId: '1' } as NFTDriverPresets.NewStreamFlowPayload); + } catch (error: any) { + // Assert + assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); + threw = true; + } + + // Assert + assert.isTrue(threw, 'Expected type of exception was not thrown'); + }); + it('it should throw an argumentError when token Id is missing from payload', async () => { // Arrange let threw = false; try { // Act - NFTDriverPresets.Presets.createNewStreamFlow({} as NFTDriverPresets.NewStreamFlowPayload); + await NFTDriverPresets.Presets.createNewStreamFlow({} as NFTDriverPresets.NewStreamFlowPayload); } catch (error: any) { // Assert assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); @@ -67,6 +106,7 @@ describe('NFTDriverPresets', () => { const validateSetDripsInputStub = sinon.stub(validators, 'validateSetDripsInput'); const payload: NFTDriverPresets.NewStreamFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userMetadata: [], balanceDelta: 1, @@ -98,7 +138,7 @@ describe('NFTDriverPresets', () => { }; // Act - NFTDriverPresets.Presets.createNewStreamFlow(payload); + await NFTDriverPresets.Presets.createNewStreamFlow(payload); // Assert assert( @@ -128,6 +168,7 @@ describe('NFTDriverPresets', () => { const validateEmitUserMetadataInputStub = sinon.stub(validators, 'validateEmitUserMetadataInput'); const payload: NFTDriverPresets.NewStreamFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userMetadata: [], balanceDelta: 1, @@ -159,7 +200,7 @@ describe('NFTDriverPresets', () => { }; // Act - NFTDriverPresets.Presets.createNewStreamFlow(payload); + await NFTDriverPresets.Presets.createNewStreamFlow(payload); // Assert assert( @@ -168,12 +209,13 @@ describe('NFTDriverPresets', () => { ); }); - it('should return the expected preset', () => { + it('should return the expected preset', async () => { // Arrange sinon.stub(validators, 'validateSetDripsInput'); sinon.stub(validators, 'validateEmitUserMetadataInput'); const payload: NFTDriverPresets.NewStreamFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userMetadata: [{ key: 'key', value: 'value' }], balanceDelta: 1, @@ -204,36 +246,37 @@ describe('NFTDriverPresets', () => { transferToAddress: Wallet.createRandom().address }; - nftDriverInterfaceStub.encodeFunctionData + nftDriverFactoryStub.setDrips .withArgs( - sinon.match((s: string) => s === 'setDrips'), - sinon.match.array.deepEquals([ - payload.tokenId, - payload.tokenAddress, - formatDripsReceivers(payload.currentReceivers), - payload.balanceDelta, - formatDripsReceivers(payload.newReceivers), - 0, - 0, - payload.transferToAddress - ]) + payload.tokenId, + payload.tokenAddress, + payload.currentReceivers, + payload.balanceDelta, + payload.newReceivers, + 0, + 0, + payload.transferToAddress ) - .returns('setDrips'); + .resolves({ data: 'setDrips' } as PopulatedTransaction); - nftDriverInterfaceStub.encodeFunctionData + nftDriverFactoryStub.emitUserMetadata .withArgs( - sinon.match((s: string) => s === 'emitUserMetadata'), + payload.tokenId, sinon.match( - (values: any[]) => - values[0] === payload.tokenId && - values[1][0].key === keyFromString(payload.userMetadata[0].key) && - values[1][0].value === valueFromString(payload.userMetadata[0].value) + ( + userMetadataAsBytes: { + key: BytesLike; + value: BytesLike; + }[] + ) => + userMetadataAsBytes[0].key === keyFromString(payload.userMetadata[0].key) && + userMetadataAsBytes[0].value === valueFromString(payload.userMetadata[0].value) ) ) - .returns('emitUserMetadata'); + .resolves({ data: 'emitUserMetadata' } as PopulatedTransaction); // Act - const preset = NFTDriverPresets.Presets.createNewStreamFlow(payload); + const preset = await NFTDriverPresets.Presets.createNewStreamFlow(payload); // Assert assert.equal(preset.length, 2); @@ -249,7 +292,7 @@ describe('NFTDriverPresets', () => { try { // Act - NFTDriverPresets.Presets.createCollectFlow(undefined as unknown as NFTDriverPresets.CollectFlowPayload); + await NFTDriverPresets.Presets.createCollectFlow(undefined as unknown as NFTDriverPresets.CollectFlowPayload); } catch (error: any) { // Assert assert.equal(error.code, DripsErrorCode.MISSING_ARGUMENT); @@ -260,13 +303,30 @@ describe('NFTDriverPresets', () => { assert.isTrue(threw, 'Expected type of exception was not thrown'); }); + it("it should throw an argumentError when signer's provider is missing", async () => { + // Arrange + let threw = false; + + try { + // Act + await NFTDriverPresets.Presets.createCollectFlow({ tokenId: '1' } as NFTDriverPresets.CollectFlowPayload); + } catch (error: any) { + // Assert + assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); + threw = true; + } + + // Assert + assert.isTrue(threw, 'Expected type of exception was not thrown'); + }); + it('it should throw an argumentError when token Id is missing from payload', async () => { // Arrange let threw = false; try { // Act - NFTDriverPresets.Presets.createCollectFlow({} as NFTDriverPresets.CollectFlowPayload); + await NFTDriverPresets.Presets.createCollectFlow({} as NFTDriverPresets.CollectFlowPayload); } catch (error: any) { // Assert assert.equal(error.code, DripsErrorCode.INVALID_ARGUMENT); @@ -282,6 +342,7 @@ describe('NFTDriverPresets', () => { const validateSqueezeDripsInputStub = sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -307,7 +368,7 @@ describe('NFTDriverPresets', () => { }; // Act - NFTDriverPresets.Presets.createCollectFlow(payload); + await NFTDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -327,6 +388,7 @@ describe('NFTDriverPresets', () => { const validateReceiveDripsInputStub = sinon.stub(validators, 'validateReceiveDripsInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -352,7 +414,7 @@ describe('NFTDriverPresets', () => { }; // Act - NFTDriverPresets.Presets.createCollectFlow(payload); + await NFTDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -366,6 +428,7 @@ describe('NFTDriverPresets', () => { const validateSplitInputStub = sinon.stub(validators, 'validateSplitInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -391,7 +454,7 @@ describe('NFTDriverPresets', () => { }; // Act - NFTDriverPresets.Presets.createCollectFlow(payload); + await NFTDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -405,6 +468,7 @@ describe('NFTDriverPresets', () => { const validateCollectInputStub = sinon.stub(validators, 'validateCollectInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -430,7 +494,7 @@ describe('NFTDriverPresets', () => { }; // Act - NFTDriverPresets.Presets.createCollectFlow(payload); + await NFTDriverPresets.Presets.createCollectFlow(payload); // Assert assert( @@ -439,7 +503,7 @@ describe('NFTDriverPresets', () => { ); }); - it('should return the expected preset', () => { + it('should return the expected preset', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -447,6 +511,7 @@ describe('NFTDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -482,40 +547,22 @@ describe('NFTDriverPresets', () => { ] }; - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'squeezeDrips'), - sinon.match.array - ) - .returns('squeezeDrips'); + dripsHubTxFactoryStub.squeezeDrips.resolves({ data: 'squeezeDrips' } as PopulatedTransaction); - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'receiveDrips'), - sinon.match.array.deepEquals([payload.userId, payload.tokenAddress, payload.maxCycles]) - ) - .returns('receiveDrips'); + dripsHubTxFactoryStub.receiveDrips + .withArgs(payload.userId, payload.tokenAddress, payload.maxCycles) + .resolves({ data: 'receiveDrips' } as PopulatedTransaction); - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'split'), - sinon.match.array.deepEquals([ - payload.userId, - payload.tokenAddress, - formatSplitReceivers(payload.currentReceivers) - ]) - ) - .returns('split'); + dripsHubTxFactoryStub.split + .withArgs(payload.userId, payload.tokenAddress, payload.currentReceivers) + .resolves({ data: 'split' } as PopulatedTransaction); - nftDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenId, payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + nftDriverFactoryStub.collect + .withArgs(payload.tokenId, payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = NFTDriverPresets.Presets.createCollectFlow(payload); + const preset = await NFTDriverPresets.Presets.createCollectFlow(payload); // Assert assert.equal(preset.length, 5); @@ -526,7 +573,7 @@ describe('NFTDriverPresets', () => { assert.equal(preset[4].data, 'collect'); }); - it('should return the expected preset when skip receiveDrips is true', () => { + it('should return the expected preset when skip receiveDrips is true', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -534,6 +581,7 @@ describe('NFTDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -549,22 +597,15 @@ describe('NFTDriverPresets', () => { ] }; - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'split'), - sinon.match.array.deepEquals([payload.userId, payload.tokenAddress, payload.currentReceivers]) - ) - .returns('split'); - - nftDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenId, payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + dripsHubTxFactoryStub.split + .withArgs(payload.userId, payload.tokenAddress, payload.currentReceivers) + .resolves({ data: 'split' } as PopulatedTransaction); + nftDriverFactoryStub.collect + .withArgs(payload.tokenId, payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = NFTDriverPresets.Presets.createCollectFlow(payload, true, false); + const preset = await NFTDriverPresets.Presets.createCollectFlow(payload, true, false); // Assert assert.equal(preset.length, 2); @@ -572,7 +613,7 @@ describe('NFTDriverPresets', () => { assert.equal(preset[1].data, 'collect'); }); - it('should return the expected preset when skip split is true', () => { + it('should return the expected preset when skip split is true', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); @@ -580,6 +621,7 @@ describe('NFTDriverPresets', () => { sinon.stub(validators, 'validateSqueezeDripsInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -595,22 +637,16 @@ describe('NFTDriverPresets', () => { ] }; - dripsHubInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'receiveDrips'), - sinon.match.array.deepEquals([payload.userId, payload.tokenAddress, payload.maxCycles]) - ) - .returns('receiveDrips'); + dripsHubTxFactoryStub.receiveDrips + .withArgs(payload.userId, payload.tokenAddress, payload.maxCycles) + .resolves({ data: 'receiveDrips' } as PopulatedTransaction); - nftDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenId, payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + nftDriverFactoryStub.collect + .withArgs(payload.tokenId, payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = NFTDriverPresets.Presets.createCollectFlow(payload, false, true); + const preset = await NFTDriverPresets.Presets.createCollectFlow(payload, false, true); // Assert assert.equal(preset.length, 2); @@ -618,13 +654,14 @@ describe('NFTDriverPresets', () => { assert.equal(preset[1].data, 'collect'); }); - it('should return the expected preset when skip receiveDrips and split are true', () => { + it('should return the expected preset when skip receiveDrips and split are true', async () => { // Arrange sinon.stub(validators, 'validateSplitInput'); sinon.stub(validators, 'validateCollectInput'); sinon.stub(validators, 'validateReceiveDripsInput'); const payload: NFTDriverPresets.CollectFlowPayload = { + signer: signerWithProviderStub, tokenId: '200', userId: '1', maxCycles: 1, @@ -640,15 +677,12 @@ describe('NFTDriverPresets', () => { ] }; - nftDriverInterfaceStub.encodeFunctionData - .withArgs( - sinon.match((s: string) => s === 'collect'), - sinon.match.array.deepEquals([payload.tokenId, payload.tokenAddress, payload.transferToAddress]) - ) - .returns('collect'); + nftDriverFactoryStub.collect + .withArgs(payload.tokenId, payload.tokenAddress, payload.transferToAddress) + .resolves({ data: 'collect' } as PopulatedTransaction); // Act - const preset = NFTDriverPresets.Presets.createCollectFlow(payload, true, true); + const preset = await NFTDriverPresets.Presets.createCollectFlow(payload, true, true); // Assert assert.equal(preset.length, 1);